:root {
  --ui-background: #fafafa;
  --ui-surface: #ffffff;
  --ui-ink: #18181b;
  --ui-gray: #8e8e93;
  --ui-line: #ececec;
  --ui-accent: #10a37f;
  --ui-comment: #18181b; /* comments are black */
  --ui-shadow-soft: 0 1px 3px rgba(0,0,0,.04), 0 8px 24px rgba(0,0,0,.06);
  --ui-shadow-float: 0 4px 12px rgba(0,0,0,.08), 0 16px 48px rgba(0,0,0,.12);
  --ui-font: -apple-system, 'Inter', 'Segoe UI', sans-serif;
  /* Glass — the chrome material. The composer and its satellites float over
     whatever a generated design paints underneath (cream, ink-black cards…):
     a frosted surface borrows that color, so the chrome belongs to every
     design instead of clashing with some of them. Alpha stays high enough
     that --ui-ink text remains readable even over a black section. */
  --ui-glass: rgba(255,255,255,.8);
  /* Status stays readable over generated designs; placeholders are lighter
     so architect suggestions never look like already-entered text. */
  --ui-glass-text: #6b6b70;
  --ui-placeholder-text: #aaaab0;
  --ui-glass-edge: transparent; /* no bright rim — the dark outline alone defines the edge */
  --ui-glass-edge-dark: rgba(0,0,0,.06);
  --ui-glass-filter: blur(18px) saturate(165%);
  /* Plain frost: one flat surface, no specular line on top (it read as a
     white hairline rather than a lens) — depth comes from the shadow only. */
  --ui-glass-shadow:
    0 4px 12px rgba(0,0,0,.06),
    0 18px 48px rgba(0,0,0,.10);
}
* { box-sizing: border-box; }
/* Manual field-of-view anchoring lives in app.js (bezPomeranja); the
   browser's native scroll anchoring must not double-compensate. */
html { overflow-anchor: none; }
body {
  margin: 0;
  background: var(--ui-background);
  color: var(--ui-ink);
  font-family: -apple-system, 'Inter', 'Segoe UI', sans-serif;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
/* UI chrome elements always use the shell font. The theme class sits on
   <body> (full-bleed), so every piece of chrome pins its own font — content
   typography is the theme's job, chrome never inherits it. */
.ui, .composer form, .log-row, .block-agent { font-family: var(--ui-font); }

/* Shell chrome that lives INSIDE the document wrapper — comment cards, their
   pins, the reply box, decorator handles — is a descendant of .doc-theme, so
   the document's own typography reaches it twice over: by inheritance, and by
   direct match, because a design's `.doc-theme p` rule hits the <p> inside a
   comment card just as happily as one in a paragraph.
   
   This is not an edge case. Every one of the designs in the database sets
   text-transform and letter-spacing, so a document laid out in caps was
   putting the agents' questions AND the user's replies in caps too.
   
   Scoped to .doc-theme, so the app's chrome outside the document — sidecar
   headings, panel labels — keeps its own uppercase. !important because the
   design is written by a model: containment cannot be a specificity race
   against CSS we did not write and cannot predict. Colour, size and
   white-space are deliberately NOT reset — the shell sets those itself where
   it needs them, and blanketing them would flatten its own styling. */
/* And a design may not paint out the shell's faces.
 *
 * The containment below is typography only, deliberately — but a comment pin
 * lives INSIDE .doc-theme (addPin appends it to the block it belongs to), so
 * `img { filter: brightness(0) }` in a generated design turned every pin into
 * a solid black shape with the avatar loaded and correct underneath it. It
 * looks exactly like a broken image and is the opposite: nothing failed.
 *
 * Only filter, blend and opacity. The shell never sets any of the three on an
 * avatar, so a value here can only have come from CSS we did not write.
 * display and visibility are deliberately NOT forced: the shell uses both
 * itself — a resolved pin hides its avatar for the tick, and a comment card
 * is visibility:hidden until it opens — and overriding them here would break
 * the shell to protect it. */
.doc-theme .ui img {
  filter: none !important;
  mix-blend-mode: normal !important;
  opacity: 1 !important;
  /* Box and placement too. A design's `img { width: 100%; margin: 2rem 0;
     border-radius: 12px }` is right for the pictures in a report and wrong
     for every face the shell draws inside it: it turned a 20px agent
     avatar into a poster beside the section, and pushed the avatar out of
     its 30px pin, which then showed as a black disc. Sizes are locked per
     element below, because they differ; everything that could move or
     frame an image is locked here. Not display or visibility: the shell
     uses both to hide what it means to hide. */
  margin: 0 !important;
  padding: 0 !important;
  border: 0 !important;
  max-width: none !important;
  max-height: none !important;
  min-width: 0 !important;
  min-height: 0 !important;
  position: static !important;
  inset: auto !important;
  float: none !important;
  transform: none !important;
  box-shadow: none !important;
  background: none !important;
  outline: none !important;
  clip-path: none !important;
  mask: none !important;
  aspect-ratio: auto !important;
  object-fit: cover !important;
}
.doc-theme .block-agent-face {
  width: 20px !important;
  height: 20px !important;
  border-radius: 50% !important;
  box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 3px var(--ui-accent) !important;
}
.doc-theme .comment-dot img,
.doc-theme .dot-top img,
.doc-theme .dot-peek img {
  width: 100% !important;
  height: 100% !important;
  border-radius: inherit !important;
}
.doc-theme .comment-avatar,
.doc-theme .comment-user-avatar {
  width: 28px !important;
  height: 28px !important;
  border-radius: 8px !important;
}
/* The madSense mark is a badge, not a portrait: a disc (see .ms-mark). */
.doc-theme .comment-avatar.ms-mark { border-radius: 50% !important; }

.doc-theme .ui, .doc-theme .ui * {
  text-transform: none !important;
  letter-spacing: normal !important;
  word-spacing: normal !important;
  font-variant: normal !important;
  font-feature-settings: normal !important;
  font-style: normal !important;
  text-indent: 0 !important;
  text-shadow: none !important;
}

/* ---------- Document (shell layout only — themes paint content + sheet bg) ---------- */
/* The themed subtree. Fills the viewport so a design's page background is
   full-bleed; chrome lives outside it and is immune to the design. */
/* The design owns how the document LOOKS. It does not get to own whether the
   page is still a page.
   
   A generated stylesheet put a decorative band's declarations into the
   .doc-theme rule instead of into its ::before — position:absolute,
   inset:0 0 auto 0, height:340px, pointer-events:none — because the model
   dropped a closing brace. .doc-theme sits on this wrapper, so the whole
   document left the flow: the page had nothing to scroll (scrollHeight came
   to exactly innerHeight, with six blocks in it) and nothing to click.
   
   These five are the properties the wrapper needs in order to remain a
   scrollable page. Everything a design actually wants from this element —
   background, colour, typography, padding, overflow-x — is left alone. The
   locks are !important because the CSS on the other side is model-written:
   this cannot be a specificity race against selectors we did not write. */
/* relative, not static: still in flow, and the containing block for the
   rail, which runs the frame's full height. */
.doc-frame {
  position: relative !important;
  inset: auto !important;
  height: auto !important;
  max-height: none !important;
  pointer-events: auto !important;
  min-height: 100vh;
}
/* A table wider than the reading column is DATA LOSS, not a style choice.
   Designs routinely put overflow-x:hidden on the wrapper (to contain a
   decorative band) and size tables by content; the columns past the edge are
   then clipped with no scrollbar and no way to reach them — a run shipped a
   twelve-column ad table whose last three columns simply did not exist for
   the reader. The table gets its own scroll container, which is the same
   pattern designs write themselves in their narrow-screen media queries.
   Only the overflow is forced; every visual property is left to the design. */
/* A table belongs to the reading column: same left edge as the prose, never
   wider, never clipped.

   Three attempts live in this rule's history. `display:block` + overflow
   scrolled a wide table, but a block-level table stops being a table box —
   its rows fall into an anonymous inner table that shrinks to CONTENT, so
   ordinary tables rendered a third of the column wide inside a full-width
   frame. It stays a real table; what is forced instead is that its cells may
   wrap, which is what makes "never wider" achievable without clipping.
   Designs reach for width:calc(100% + 92px) with negative side margins when
   columns are tight; bleeding out and fitting in are incompatible, and the
   result was a slab starting left of every paragraph and still cut off on
   the right. */
.doc-theme .block:not([data-kind="stats"]) .block-body table {
  display: table !important;
  width: 100% !important;
  margin-left: 0 !important;
  margin-right: 0 !important;
}
/* A stats section's table is three cells wide and never needs the table
   guard above; it is exempt, and it gets a shell default instead: a compact
   grid of figures, two or three across, that a design restyles rather than
   rebuilds. This is the floor, not a lock, and the :where() is what makes it
   one: written plainly, the selector outweighed a design's own
   `[data-kind="stats"] td` rule, so a design's padding never landed and
   labels sat on the row's border while the number beside them was inset. */
:where(.block[data-kind="stats"] .block-body) table {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px 24px;
  width: 100%;
}
:where(.block[data-kind="stats"] .block-body) tbody { display: contents; }
:where(.block[data-kind="stats"] .block-body) tr { display: flex; flex-direction: column; gap: 3px; padding: 8px 0; }
:where(.block[data-kind="stats"] .block-body) td { display: block; padding: 0; border: 0; }
:where(.block[data-kind="stats"] .block-body) td:first-child { font-size: 12px; letter-spacing: .06em; text-transform: uppercase; opacity: .7; }
:where(.block[data-kind="stats"] .block-body) td:nth-child(2) { font-size: 30px; line-height: 1.1; font-variant-numeric: tabular-nums; }
:where(.block[data-kind="stats"] .block-body) td[data-change] { font-size: 13px; }
/* Locks, so a design cannot undo the shapes above (export shares these):
   - a stats figure's surface is its row; a design that boxed every CELL
     produced three stacked capsules per figure, which read as a mistake;
   - in a callout that quotes someone, the writer's framing paragraph stays
     in the body voice, or the quote and the framing look like one voice;
   - a section never scrolls: the app scrolls a wide table inside its own
     wrapper, and a section made a scroll container hid its comment pins
     until the reader scrolled sideways. */
.doc-theme .block[data-kind="stats"] .block-body td {
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  background: transparent !important;
}
/* And no box around the figure either: three low builds in a row put every
   figure in a white card, which is the one thing a stats section did not
   need. Figures sit on the page ground; a hairline (a border on the row)
   is still the design's to draw. */
.doc-theme .block[data-kind="stats"] .block-body tr {
  background: transparent !important;
  box-shadow: none !important;
  border-radius: 0 !important;
}
/* The grid's structure is locked; only its structure. The floor above is
   zero-specificity so a design's padding and type land, and a design's
   generic table rules then landed too: a `td { text-align: right }` sent
   the figures drifting from their labels, a `td:first-child { width }`
   wrapped IMPRESSIONS one letter-group per line, and a `td { display:
   inline-block }` ran every label into the next. The row is a column, the
   cells are blocks that share a left edge and take the row's width, and
   nothing breaks a word. Colour, face, size, spacing and rules stay the
   design's. */
.doc-theme .block[data-kind="stats"] .block-body table { display: grid !important; width: 100% !important; float: none !important; }
.doc-theme .block[data-kind="stats"] .block-body tbody { display: contents !important; }
.doc-theme .block[data-kind="stats"] .block-body tr {
  display: flex !important; flex-direction: column !important;
  width: auto !important; min-width: 0 !important; max-width: none !important;
  float: none !important;
}
.doc-theme .block[data-kind="stats"] .block-body td {
  display: block !important;
  width: auto !important; min-width: 0 !important; max-width: none !important;
  text-align: left !important;
  white-space: normal !important; overflow-wrap: normal !important; word-break: normal !important; hyphens: none !important;
  float: none !important; position: static !important;
}
.doc-theme .block[data-kind="callout"] .block-body:has(blockquote) > p {
  font-size: inherit !important;
  font-style: normal !important;
  color: inherit !important;
}
.doc-theme .block, .doc-theme .block-body { overflow: visible !important; }

/* A table's floor, in any section: full width, a left-aligned header row
   set off by a rule, a hairline under each row, and room in the cells. A
   design's own table rules replace every line of it (zero specificity via
   :where), but a design that only styled tables inside the kinds it knew
   about left a table in a prose section at the browser's defaults — a
   centred bold header and no rules, the one thing on the page that looked
   like Word. Stats tables are exempt: their grid has its own floor. */
:where(.doc-theme .block:not([data-kind="stats"]) .block-body) table { width: 100%; border-collapse: collapse; margin: 14px 0; }
:where(.doc-theme .block:not([data-kind="stats"]) .block-body) th { text-align: left; font-weight: 600; padding: 8px 10px 8px 0; border-bottom: 1px solid currentColor; vertical-align: bottom; }
:where(.doc-theme .block:not([data-kind="stats"]) .block-body) td { text-align: left; padding: 8px 10px 8px 0; border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent); vertical-align: baseline; }

/* The scroll home for a table genuinely wider than the column (eleven
   columns of campaign metrics). Unscoped from .doc-theme on purpose: it must
   work mid-build, before any design lands. */
.block-body .table-scroll { overflow-x: auto; }
.block-body .table-scroll > table { min-width: 100%; }
/* Prose fills the reading column, like everything else in it. Designs cap
   body copy at a measure (66ch is the usual reach) which is sound advice for
   a full-width page — but the column here is ALREADY the measure, so the cap
   applies twice and paragraphs end up visibly narrower than the tables and
   headings beside them. One measure, set in one place. */
.doc-theme .block-body p,
.doc-theme .block-body ul,
.doc-theme .block-body ol,
.doc-theme .block-body blockquote {
  max-width: none !important;
}
.doc-theme .block-body th,
.doc-theme .block-body td {
  /* white-space:nowrap in a column narrower than the content is how columns
     disappear off the right edge. Wrapping is the honest alternative. */
  white-space: normal !important;
  overflow-wrap: break-word;
}
/* Tier 1 bar charts (ELEMENTS.md §2). The block's table IS the chart: code
   stamps --v (0..1) on each data row at render time, and this draws a real
   bar in the value cell — a solid inline shape from a common left baseline,
   with the figure riding its end, the way value labels sit on any bar
   chart. The first cut painted a faint tint BEHIND the cell instead, and it
   read as zebra shading, not as a chart. Color comes from the design's own
   text color unless a design defines --chart-bar; the longest bar stops at
   --chart-span so the longest figure still has room. Kept in parallel with
   LAYOUT_CSS in server/render.ts (the export carries the same rules). Two
   selectors each: the bare one works mid-build before any design lands, the
   .doc-theme one outweighs any generic td rule a designer writes. The bar
   is a ::before shape, so stripping this CSS leaves the chart as its own
   data table — that degradation is the point. */
table.chart-bars td:last-child,
.doc-theme .block-body table.chart-bars td:last-child {
  position: static !important; display: table-cell !important;
  /* Bars grow from the left, so the cell must not re-center or right-align
     the bar+number pair a design's td rule would. The nowrap keeps the
     figure ON the bar's baseline rather than wrapped beneath it; it may
     out-!important the anti-clipping floor above because the value lane is
     sized (45% column, 72% span) so the figure always has its ~28%. */
  text-align: left;
  white-space: nowrap !important;
  font-variant-numeric: tabular-nums;
}
/* Only rows the stamper valued (tr carries --v inline) get a bar: a Total
   row keeps its number and gets no shape, same as the header. */
table.chart-bars tr[style] > td:last-child::before,
.doc-theme .block-body table.chart-bars tr[style] > td:last-child::before {
  content: '';
  display: inline-block !important; position: static !important; inset: auto !important;
  vertical-align: -0.12em;
  width: calc(var(--v, 0) * var(--chart-span, 72%));
  height: 0.85em !important;
  border-radius: 2px;
  margin-right: 0.55em;
  background: var(--chart-bar, color-mix(in srgb, currentColor 58%, transparent));
}
table.chart-bars td:last-child { width: 45%; }

/* Paired bars (ELEMENTS.md §2): this period against the last. The table
   carries data-paired and every drawn row TWO stamps — --v for the current
   period in the last cell, --p for the prior one in the cell before it —
   normalised together against the max across BOTH columns, so a number that
   fell can never draw a bar that grew. Each period keeps its own column
   rather than stacking two shapes in one cell: the ghost then sits under its
   own header, the figures stay beside their own bars, and the degradation is
   still the plain table. Same ink as the current bar, faded, unless a design
   sets --chart-ghost. */
table.chart-bars[data-paired] td:nth-last-child(2),
.doc-theme .block-body table.chart-bars[data-paired] td:nth-last-child(2) {
  position: static !important; display: table-cell !important;
  text-align: left;
  white-space: nowrap !important;
  font-variant-numeric: tabular-nums;
}
table.chart-bars[data-paired] tr[style] > td:nth-last-child(2)::before,
.doc-theme .block-body table.chart-bars[data-paired] tr[style] > td:nth-last-child(2)::before {
  content: '';
  display: inline-block !important; position: static !important; inset: auto !important;
  vertical-align: -0.12em;
  width: calc(var(--p, 0) * var(--chart-span, 72%));
  height: 0.85em !important;
  border-radius: 2px;
  margin-right: 0.55em;
  background: var(--chart-ghost, color-mix(in srgb, currentColor 28%, transparent));
}
table.chart-bars[data-paired] td:nth-last-child(2),
table.chart-bars[data-paired] td:last-child { width: 30%; }

/* Line and donut charts (ELEMENTS.md §2). Same contract as the bars, one
   step further: the analyst writes a table, code draws the picture from ITS
   numbers at render time, and nothing is stored — strip this stylesheet and
   the data table the chart was drawn from is still there. The <figure> sits
   immediately before its table inside the block body and states no number
   the table does not. NO COLOUR LIVES IN THE SVG: every stroke is
   currentColor or one of --chart-bar (primary line) / --chart-ghost
   (comparison line) / --chart-1..--chart-6 (donut segments), written inline
   with a currentColor fallback, so a design colours a chart without knowing
   charts exist. These rules are layout only. Kept in parallel with
   LAYOUT_CSS in server/render.ts (the export carries the same rules). */
.block-body .chart,
.doc-theme .block-body .chart {
  display: block;
  margin: 4px 0 14px;
  max-width: 100%;
  padding: 0;
  background: none;
  border: 0;
}
.block-body .chart-line svg,
.doc-theme .block-body .chart-line svg {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
}
.block-body .chart-donut svg,
.doc-theme .block-body .chart-donut svg {
  display: block;
  width: 160px;
  height: 160px;
  max-width: 100%;
  margin: 0 auto;
}
/* The axis text and the zero baseline carry their own colourless attributes
   in the SVG, so a chart still reads if this stylesheet never arrives. These
   restate them as real rules, which is what makes them a hook: a design that
   wants quieter ticks or a firmer baseline overrides these two selectors. */
.block-body .chart .chart-tick,
.doc-theme .block-body .chart .chart-tick {
  font-size: 11px;
  fill: currentColor;
  stroke: none;
}
.block-body .chart .chart-baseline,
.doc-theme .block-body .chart .chart-baseline {
  stroke: currentColor;
  stroke-width: 1;
}
/* The table under a ring IS its legend: every drawn row carries its own
   segment colour as --c, and this swatch is what ties a row to its arc — so
   no percentage has to be written into the picture, and a Total row (which
   is not a segment) correctly has no swatch. */
table.chart-donut tr[style] > td:first-child::before,
.doc-theme .block-body table.chart-donut tr[style] > td:first-child::before {
  content: '';
  display: inline-block;
  width: 0.62em;
  height: 0.62em;
  border-radius: 2px;
  margin-right: 0.5em;
  vertical-align: -0.02em;
  background: var(--c, currentColor);
}

/* Post pictures under a section (docs/ELEMENTS.md §3). A row of thumbnails
   at reading size that WRAPS rather than shrinking below legibility — four
   squeezed into one line would be decoration nobody can see. The designer
   styles figure/figcaption like any other element in the vocabulary; this is
   only the floor that keeps a phone-resolution photo from running the width
   of the column. Kept in parallel with LAYOUT_CSS in server/render.ts. */
.block-body .post-media {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin: 20px 0 0;
}
/* Every figure gets the SAME frame — a fixed picture box and a two-line
   caption — so the number strips underneath all start on one line. Sized
   figures rather than a shared table on purpose: the metrics differ by
   surface (an Instagram post has reach and likes, a Page post reactions and
   shares, an ad spend and clicks), so one table would need one set of column
   headings that fits none of them. Each picture keeps its own numbers; the
   grid is what makes them read as a row. */
.block-body .post-figure { margin: 0; flex: 0 0 170px; max-width: 170px; }
/* The fixed box is this WRAPPER, not the image. One height for all of them
   so everything below lines up, images bottom-aligned inside it, and nothing
   cropped — these creatives carry their message IN the picture, and a square
   crop of a 9:16 Reel cuts the line it was written around.
   Keeping the image element exactly the size of the picture matters for a
   second reason: the leftover space in a cell then belongs to the figure and
   stays transparent. A generated design shipped
   `.doc-theme img{background:#fff;border:1px solid var(--rule)}` — a blind
   guess about images it was never told existed — which painted that empty
   space as a white slab. Now the same rule frames the picture itself, which
   is what the designer meant. */
.block-body .post-shot {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: 200px;
}
/* The linked caption (docs/ELEMENTS.md §3). The NAME is the link, not the
   picture: a clickable thumbnail gives a reader no way to tell a linked
   figure from an unlinked one, and no way to look at the picture without
   arming a navigation.

   A flex row so the arrow can never be swallowed by the two-line clamp on the
   text — the clamp lives on .post-when, and the icon is its sibling rather
   than its content. Colour is inherited instead of link-blue (a report is not
   a web page); the underline is what marks it.

   Three classes deep on purpose: design CSS is appended AFTER this file, so
   an equal-specificity rule in a generated design would win on order, and `a`
   is the only anchor a document body can contain (the sanitizer drops the
   rest) — so a design guessing at `a`, the way one once guessed at
   `img{background:#fff;border:1px solid}`, would hit exactly this. */
.block-body .post-figure a.post-link {
  display: flex;
  align-items: flex-start;
  gap: 3px;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.block-body .post-figure a.post-link .post-when {
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* The caption sits at 0.72 opacity so it stays quieter than the numbers; a
   link the reader is hovering should not. */
.block-body .post-figure a.post-link:hover .post-when { opacity: 1; }
.block-body .post-figure .post-ext {
  flex: none;
  width: 9px;
  height: 9px;
  margin-top: 0.28em;
  opacity: 0.62;
}
.block-body .post-figure img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: 4px;
}
.block-body .post-figure figcaption {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.35;
}
/* Two lines, always: ad names run long ("Ongoing - Video CA - MagnaSLIM 2.2
   - UD 1/14/26") and a caption that wraps to a second line would push its own
   strip out of line with the others. */
.block-body .post-figure .post-when {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.7em;
  opacity: 0.72;
}
/* The row's own numbers under the picture (docs/ELEMENTS.md §1: the image is
   the claim, these are its evidence — they are what let a reader tie a
   thumbnail back to the line it came from in the table above). A <dl>, not a
   <table>: designs style tables heavily, and a thumbnail caption wearing a
   report table's chrome reads as a mistake. currentColor throughout, so the
   strip takes the document's palette without the designer knowing it exists. */
.block-body .post-stats {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1px 8px;
  margin: 6px 0 0;
  font-size: 11px;
}
.block-body .post-stats dt {
  margin: 0;
  opacity: 0.6;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.block-body .post-stats dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.block-body .post-stats dt,
.block-body .post-stats dd {
  border-top: 1px solid currentColor;
  border-color: color-mix(in srgb, currentColor 14%, transparent);
  padding-top: 2px;
}
/* A picture whose bytes would not load: the caption still says what it was,
   which is more useful than a broken-image icon. */
.block-body .post-figure-missing figcaption { opacity: 0.5; font-style: italic; }

/* A design shipped `.doc-theme .doc{max-width:none}`, and the prose ran the
   full width of a desktop window — measurably worse to read, and not what the
   contract the designer is given describes ("a centered ~760px reading
   column"). Designs may still make it narrower through padding; they may not
   remove it. */
.doc-theme .doc { max-width: 760px !important; }
/* Designs cap the title (max-width:12ch and the like) for stacked display
   type; four words then wrap into a half-column tower. The title spans the
   column, like everything else in it. Size stays the design's to choose. */
.doc-theme h1.doc-title { max-width: none !important; width: auto !important; }
.doc {
  max-width: 760px;
  /* The shell owns the top strip: the chrome floats over the first 56px,
     and this margin is the whole clearance. Designs used to be asked to
     leave room of their own as well, and the two added up to a quarter of
     a screen of nothing. Locked, so a design's padding-top on the column
     cannot add to it. The sheet itself stays full-bleed. */
  margin: 0 auto !important;
  padding: 0 20px;
  padding-top: 0 !important;
  /* Static, so the rail (absolute) is placed against the FRAME and sits
     in the page margin. A design that made the column relative pulled the
     rail inside it, over the first letters of every line. */
  position: static !important;
}
/* The clearance is padding on the FRAME, which the design paints, not a
   margin on the column: a top margin collapses through the frame and the
   design's ground then started 72px down, with the app's own white above it
   under the chrome. */
.doc-frame { padding-top: 72px !important; }
/* No big-letter openings: any ::first-letter or ::first-line rule is
   dropped from a design at cleaning time (server/design-clean.ts). It used
   to be locked here with a neutral rule instead, but even a neutral
   ::first-letter creates the pseudo-element, and browsers paint neither
   the selection nor the caret on it: the first letter of every paragraph
   looked unselectable. */
.doc > :first-child { margin-top: 0 !important; padding-top: 0 !important; }
/* The lead reads at the body's size. Every design made it larger, some by
   half again, and the owner has said no: the opening is set apart by air
   and by the bold inside it, not by a bigger face. */
.doc-theme .block[data-kind="lead"] .block-body p { font-size: inherit !important; line-height: inherit !important; }
/* Always room to scroll the last block clear of the composer + chips. Not
   on .doc (themes restyle it) — a sibling spacer themes never address. */
.bottom-space { height: 48px; }
html { scroll-padding-bottom: 120px; }
.empty { display: none; } /* the onboarding notes (.intro) are the blank state */

/* ---------- Intro (onboarding on the blank document) ---------- */
/* Two notes in the middle of the page; from each, a hand-drawn ink arrow
   with one playful loop — up to the Design button, down to the prompt.
   Paths come from app.js (drawIntro) so the tips land at any window size;
   strokes draw themselves on, the open arrowheads fade in at the end. */
.intro { display: none; position: fixed; inset: 0; z-index: 40; pointer-events: none; color: var(--ui-ink); }
body.no-document .intro { display: block; }
/* Defensive guard for streamed document creation: even if blank-state
   bookkeeping arrives out of order, onboarding can never cover real work. */
body.no-document:has(#doc > .doc-title, #doc > .block) .intro { display: none; }
/* Sent once: the onboarding has been acted on and does not come back, however
   the next blank state is reached. See endIntro() in app.js. */
body.intro-done .intro { display: none; }
/* A section that was not finished: one quiet line under it, and the offer
   to write it again. Muted on purpose: a stopped run marks every section it
   was writing, and a wall of red for one click would be wrong. */
.block-failed {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 6px 0 10px; font: 13px/1.4 var(--ui-font); color: var(--ui-glass-text);
}
.block-failed-text { min-width: 0; }
.block-failed-retry {
  border: 1px solid var(--ui-line); background: var(--ui-surface); color: var(--ui-ink);
  border-radius: 999px; padding: 4px 11px; font: 500 12.5px/1.3 var(--ui-font); cursor: pointer;
}
.block-failed-retry:hover { border-color: var(--ui-ink); }
.block-failed-retry:disabled { opacity: .5; cursor: default; }
/* The rail: a decorator that lives beside the column rather than in it, the
   one place a design can put a running head or a motif that gives the
   document personality without touching the reading measure. Positioned
   here so the design only has to voice it; gone on a narrow screen, where
   there is no margin to live in. */
.doc > .doc-decorator[data-decorator-placement="rail"] {
  /* A strip down the far left of the page, the page's full height: the
     design's own surface, painted with --rail (a colour, a gradient, a
     repeating pattern). Its text is the branding kept with the document;
     hidden here, shown by a design that sets a colour on it. */
  position: absolute; top: 0; bottom: 0; left: 0;
  width: var(--rail-width, 56px); box-sizing: border-box;
  /* A pattern by default, never a flat colour: a diagonal hatch in the
     document's own ink, faint. A design replaces the whole thing with --rail
     (any background: stripes, dots, checks, a gradient over a pattern). */
  background: var(--rail, repeating-linear-gradient(-45deg, var(--rail-ink, color-mix(in srgb, currentColor 30%, transparent)) 0 1.5px, transparent 1.5px 9px));
  padding: 18px 0 0 0;
  writing-mode: vertical-rl; text-align: left;
  /* No text, ever: the strip is a pattern. The decorator's text is kept
     with the document as its brand line, and never drawn. */
  color: transparent !important; font-size: 0 !important; user-select: none;
  white-space: nowrap; overflow: hidden;
  pointer-events: none;
  /* Where it is, is not the design's: a margin-top moved it a third of
     the way down the page. The design paints it; the shell places it. */
  margin: 0 !important; transform: none !important;
  top: 0 !important; bottom: 0 !important; left: 0 !important;
}
@media (max-width: 960px) {
  .doc > .doc-decorator[data-decorator-placement="rail"] { display: none; }
}

/* Arriving from the landing page: its prompt box has just landed where the
   composer paints. See fly() in meta.html and begin() in app.js. */
body.arriving .composer form { animation: arrive .3s ease-out both; }
@keyframes arrive { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { body.arriving .composer form { animation: none; } }
.intro-middle {
  position: absolute; left: 50%; top: 40%; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 40px;
  text-align: center;
}
.intro-note {
  margin: 0; opacity: .82;
  font: italic 16px/1.5 'Iowan Old Style', 'Palatino', Georgia, serif;
  letter-spacing: .005em; white-space: nowrap;
}
.intro-sub { color: var(--ui-gray); }

/* Typing. The text is clipped from the right and revealed in steps(one per
   character); the caret is a separate absolutely-positioned element that
   walks the same steps, so the two never drift apart. Both animations are
   assigned by app.js — it owns the running order, and steps() cannot read a
   custom property. Base state here is "not yet typed", so nothing flashes
   before its turn. */
.intro-typed { position: relative; display: inline-block; }
.intro-chars { display: inline-block; clip-path: inset(0 100% 0 0); }
.intro-caret {
  position: absolute; top: .12em; bottom: .12em; left: 0;
  width: 1.5px; background: currentColor; opacity: 0;
}
@keyframes intro-type { to { clip-path: inset(0 0 0 0); } }
/* No `forwards`: when it ends the caret reverts to opacity 0 and vanishes,
   which is exactly what a caret should do when it stops typing. */
@keyframes intro-caret { from { left: 0; opacity: .7; } to { left: 100%; opacity: .7; } }
.intro-canvas { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* Undrawn until app.js hands each one its turn. */
.intro-line { stroke-dasharray: var(--ui-length, 600); stroke-dashoffset: var(--ui-length, 600); }
.intro-head { opacity: 0; }
@keyframes intro-draw { to { stroke-dashoffset: 0; } }
@keyframes intro-head-in { to { opacity: 1; } }

/* Nothing performs for someone who asked for stillness: the notes and both
   arrows are simply there. */
@media (prefers-reduced-motion: reduce) {
  .intro-chars { clip-path: none !important; animation: none !important; }
  .intro-caret { display: none; }
  .intro-line { stroke-dashoffset: 0 !important; animation: none !important; }
  .intro-head { opacity: 1 !important; animation: none !important; }
}

/* ---------- Building (assembly while agents work) ---------- */
/* Wireframe blocks landing on a wireframe page, one 5.2s cycle: the page
   draws itself on, four blocks drop in, everything fades, again. All the
   stagger lives inside keyframe percentages rather than animation-delay so
   the loop stays in phase forever. Lines inherit currentColor. */
.building {
  position: fixed; inset: 0; z-index: 5;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none; color: var(--ui-ink);
}
.building svg { display: block; }

.gr-line { stroke-dasharray: 1; animation: gr-base 5.2s ease-out infinite; }
@keyframes gr-base {
  0%       { stroke-dashoffset: 1; opacity: 0; }
  2%       { opacity: .34; }
  10%, 90% { stroke-dashoffset: 0; opacity: .34; }
  100%     { stroke-dashoffset: 0; opacity: 0; }
}

/* Each block: invisible above its slot → lands bright → settles into the
   stack → fades with the rest. Rest positions are SVG attribute transforms
   on the parent group, so the CSS animation only ever moves relative. */
.gr-slab { animation: 5.2s ease-out infinite; }
.gr-b1 { animation-name: gr-b1; }
.gr-b2 { animation-name: gr-b2; }
.gr-b3 { animation-name: gr-b3; }
.gr-b4 { animation-name: gr-b4; }
@keyframes gr-b1 {
  0%, 12%   { transform: translateY(-40px); opacity: 0; }
  20%       { transform: translateY(0);     opacity: .95; }
  27%, 90%  { transform: translateY(0);     opacity: .45; }
  100%      { transform: translateY(0);     opacity: 0; }
}
@keyframes gr-b2 {
  0%, 28%   { transform: translateY(-40px); opacity: 0; }
  36%       { transform: translateY(0);     opacity: .95; }
  43%, 90%  { transform: translateY(0);     opacity: .45; }
  100%      { transform: translateY(0);     opacity: 0; }
}
@keyframes gr-b3 {
  0%, 44%   { transform: translateY(-40px); opacity: 0; }
  52%       { transform: translateY(0);     opacity: .95; }
  59%, 90%  { transform: translateY(0);     opacity: .45; }
  100%      { transform: translateY(0);     opacity: 0; }
}
@keyframes gr-b4 {
  0%, 60%   { transform: translateY(-40px); opacity: 0; }
  68%       { transform: translateY(0);     opacity: .95; }
  75%, 90%  { transform: translateY(0);     opacity: .45; }
  100%      { transform: translateY(0);     opacity: 0; }
}

/* The feed: a dashed line streaming down into the stack — the power that
   is doing the building. Offset runs the whole cycle so the flow never
   visibly resets. */
.gr-feed { animation: gr-feed 5.2s linear infinite; }
@keyframes gr-feed {
  0%   { stroke-dashoffset: 0; opacity: 0; }
  8%   { opacity: 0; }
  14%  { opacity: .4; }
  80%  { opacity: .4; }
  90%  { opacity: 0; }
  100% { stroke-dashoffset: -54; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .building .gr-line, .building .gr-slab, .building .gr-feed { animation: none; }
  .building .gr-line { stroke-dashoffset: 0; opacity: .34; }
  .building .gr-slab { opacity: .45; }
  .building .gr-feed { opacity: .25; }
}
@media (max-width: 900px) { .intro-canvas { display: none; } }

/* ---------- Block (interaction chrome only — typography owned by theme) ---------- */
.block { position: relative; margin-bottom: 6px; }
.block-body {
  outline: none;
  border-radius: 16px;
  padding: 10px 16px;
  margin: 0 -16px;
}
/* No hover chrome on blocks — hovering reveals the comment affordance
   instead. Editing a block should not add visual chrome. */
.block-body:focus { outline: none; }

/* "Leave a comment" appears on block hover, in the pins' lane — a quiet
   outline ghost, and only after the cursor has RESTED on the block for a
   beat (transition-delay), so it doesn't chase you while scrolling. */
/* The pin lane sits INSIDE the block box (right: 6px, not outside it):
   generated designs are free to clip the document (`overflow: hidden` on a
   card, narrow columns, …), and anything outside the block would get cut. */
/* Black, not a pale outline. This is the one affordance that says a document
   can be talked to, and as a light chip on a light page it read as disabled
   chrome — people did not find it. */
.block-comment-button {
  position: absolute;
  top: var(--pin-top, 8px);
  left: calc(100% + 22px + var(--pin-x, 0px));
  z-index: 4;
  width: 30px; height: 30px;
  border-radius: 50% 50% 50% 6px;
  /* Light now. It was black because it sat ON the text and had to look
     deliberate rather than like disabled chrome; out in its own strip there
     is nothing for it to compete with, and a black dot in the margin reads
     louder than the thing it is for. */
  border: 1px solid var(--ui-glass-edge-dark);
  background: var(--ui-surface); color: var(--ui-ink);
  font-size: 15px; font-weight: 500; line-height: 1;
  cursor: default;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--ui-shadow-soft);
  opacity: 0; transform: scale(.85);
  transition: opacity .15s, transform .15s, color .15s, border-color .15s;
  /* Waits before going. There is a 22px gap between the text and the strip
     now, and leaving the block to reach the button crossed it faster than
     the button faded — it vanished on the way to being pressed. */
  transition-delay: .5s, .5s, 0s, 0s;
  pointer-events: none;
}
/* The block's hover region reaches INTO the strip.
   A bridge on the button itself was not enough, for a reason that is easy to
   miss: `pointer-events` cannot be transitioned. The moment the pointer left
   the block, `.block:hover` stopped matching and the button went
   non-interactive instantly — the .5s delay held its opacity and nothing
   else, so it was visible and unhoverable. Approaching through an existing
   pin worked only because the pin kept something hovered on the way.
   So the block itself extends over the strip: crossing the gap never leaves
   it, and the button stays both visible and live. Behind everything in the
   lane (z-index auto against the pins' 5 and the button's 4), so it catches
   only the space between them. */
.block::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: 100%;
  width: 96px;
}
@media (max-width: 1000px) {
  /* No strip to reach into, and a 96px invisible margin-grabber beside every
     block is not something to leave lying around. */
  .block::after { display: none; }
}
.block:hover > .block-comment-button,
.block-comment-button:hover,
.block-comment-button.open {
  opacity: 1; transform: scale(1); pointer-events: auto;
  transition-delay: .45s, .45s, 0s, 0s;
}
/* Once it is out, hovering the button itself holds it there with no delay of
   its own — the .5s above is for the crossing, not for a pointer that has
   already arrived. */
.block-comment-button:hover { transition-delay: 0s; opacity: 1; }
.block-comment-button.open { transition-delay: 0s; }
.block-comment-button:hover { opacity: .84; }
/* Opening a comment box does NOT swap the + for a pin. The entry sits in the
   same spot as the button, so its dot landed on top of it and the + appeared
   to turn into a stray "Y" (a placeholder initial) before the box arrived —
   the button reading as if it became something else rather than opened
   something.

   Invisible, NOT display:none. The box opens on pointerenter and stays open
   while the pointer is over it, so this dot is the hover bridge between the
   button and the card: removing it from layout left the card with nothing
   under the cursor, `.comment-wrap:hover` never matched, and the box was
   created invisible — open, and unusable. So it keeps its area and loses its
   paint, and the + shows through underneath. */
.comment-wrap.entry > .comment-dot {
  background: transparent;
  border-color: transparent;
  color: transparent;
  box-shadow: none;
}

/* Re-runs happen IN PLACE: the existing content stays fully readable while
   a writer works (the pulsing chip is the only signal) and the new version
   morphs in where the old one stood. No graying, no skeleton collapse. */

/* Who is working on this section, while they work. No pill and no border:
   the face and the moving dots carry it, so a building document reads as a
   team at work rather than a row of status badges. Replaced a chip that said
   "assigned to agent — <section title>", which named the section the reader
   was already looking at and called every agent "agent". */
.block-agent {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--ui-gray);
  user-select: none;
  /* In the left rail (see .block-rail), so it wraps to the rail's width
     instead of running along under the heading. Right-aligned, which puts
     the live end of the line — the breathing dots — nearest the section it
     is about. Wrapping keeps the words in reading order; reversing the row
     to move the face would have reversed the sentence with it. */
  flex-wrap: wrap;
  justify-content: flex-end;
  margin: 0;
}
@media (max-width: 1200px) {
  .block-agent { justify-content: flex-start; }
}
.block-agent-face {
  width: 20px; height: 20px; border-radius: 50%;
  object-fit: cover; flex: none;
  /* The ring breathes instead of a separate dot: one moving thing, attached
     to the agent it is about. */
  box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 3px var(--ui-accent);
  animation: agent-breathe 1.6s ease-in-out infinite;
}
/* The quiet form, used while a section is written for the FIRST time. On a
   build every section is being written at once by the same agent, so the
   named line repeats down the whole margin; this says the same thing once per
   section without saying it in words. Same box as the named row so the rail
   does not shift when a later revision replaces one with the other. */
.block-agent-quiet {
  min-height: 20px;
  cursor: default;
}
.block-agent-pip {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--ui-accent);
  /* Same rhythm as the face's ring, so a document with both on screen
     breathes as one thing rather than two clocks. */
  animation: agent-breathe 1.6s ease-in-out infinite;
  box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 3px color-mix(in srgb, var(--ui-accent) 30%, transparent);
}
.block-agent-name { font-weight: 600; color: var(--ui-ink); }
.block-agent-verb { opacity: 0.75; }
/* No typing dots. They were a second moving thing saying what the breathing
   ring already says — and being <i> elements inside .doc-theme, a design's
   own `i` rule reached them and rendered them as coloured triangles. */
@keyframes agent-breathe {
  50% { box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 5px color-mix(in srgb, var(--ui-accent) 35%, transparent); }
}
/* Motion is decoration here: the words already say what is happening. */
@media (prefers-reduced-motion: reduce) {
  .block-agent-face { animation: none; }
  .block-agent-pip { animation: none; }
}

/* ---------- The left rail ----------
   Everything the SHELL has to say about a section — who is working on it,
   what a revision just changed — lives beside the section rather than inside
   it. Two reasons, both learned the hard way:

   The agent line used to sit under the heading INSIDE .block-body, which put
   shell chrome inside the editable region: it had to be marked uneditable,
   then stripped again by bodyHtmlForSave, and one missed strip away from
   being saved into the document as prose. It also pushed the text down as it
   appeared and popped it back as it went.

   And it belongs where the reader is not reading. Comments already own the
   right (.comment-wrap pins to right: 6px), so this is the left. Absolute, so
   it can never affect the flow of the prose it annotates. */
.block-rail {
  position: absolute;
  top: 12px;
  /* Outside the block box entirely — .block-body is inset by -16px, so this
     clears the text column and the padding both. */
  right: calc(100% + 22px);
  width: 190px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  text-align: right;
  user-select: none;
  pointer-events: none; /* never steal a click meant for the document */
  z-index: 4;
}
.block-rail > * { pointer-events: auto; }

/* Below the width where a real margin exists, the rail stops being a margin
   and becomes a line above the section. Same DOM, same elements — only the
   positioning changes, so nothing in JS has to know which layout is live.
   760px of document plus two 212px rails is ~1184; under that the margin
   would sit on top of the text or off the screen. */
@media (max-width: 1200px) {
  .block-rail {
    position: static;
    width: auto;
    align-items: flex-start;
    text-align: left;
    margin: 0 0 10px;
  }
}

/* ---------- The revision receipt ----------
   What a prompt changed, beside the section it changed. Quoted rather than
   summarised: a reader who did not type it — a collaborator, or you on
   Monday — needs to know what was ASKED. It settles after a few seconds into
   the bare word, so a section stays marked as changed for the session
   without a wall of quoted prompts sitting in the margin. */
.receipt {
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--ui-gray);
  border-right: 2px solid color-mix(in srgb, var(--ui-accent) 55%, transparent);
  padding: 1px 10px 1px 0;
  max-width: 100%;
  animation: receipt-in .32s cubic-bezier(.2,.7,.3,1) both;
}
@media (max-width: 1200px) {
  .receipt {
    border-right: 0;
    border-left: 2px solid color-mix(in srgb, var(--ui-accent) 55%, transparent);
    padding: 1px 0 1px 10px;
  }
}
.receipt-label {
  display: block;
  font-weight: 600;
  color: color-mix(in srgb, var(--ui-accent) 75%, var(--ui-gray));
  letter-spacing: .02em;
}
/* The prompt itself, clamped to two lines: a long instruction is a
   paragraph, and the margin is not where a paragraph goes. Hover gives the
   whole thing, in BOTH states — before it settles it is clamped, after it
   settles it is folded away, and either way the reader's question is the
   same one ("what did I actually ask?") with the same answer. */
.receipt-asked {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 2px;
  font-style: italic;
  opacity: .78;
  transition: opacity .25s ease, max-height .25s ease, margin-top .25s ease;
}
/* Settled: the quote folds away, the mark stays. */
.receipt.settled .receipt-asked {
  opacity: 0;
  margin-top: 0;
  max-height: 0;
  transition: opacity .5s ease, max-height .5s ease, margin-top .5s ease;
}
/* Hover, from either state: the whole brief. Clamped by LINES rather than by
   height — a max-height cap cut the last line through the middle of its
   letters, which reads as broken rendering rather than as "there is more".
   line-clamp always ends on a whole line and adds its own ellipsis. Twelve
   is past the length of any brief the architect writes; the cap is a
   backstop, not a design. The rail is absolute, so growing moves nothing. */
.receipt:hover .receipt-asked {
  -webkit-line-clamp: 12;
  max-height: none;
  opacity: .9;
  margin-top: 2px;
}
@keyframes receipt-in {
  from { opacity: 0; transform: translateY(-4px); }
}

/* One sweep of light across the section a revision rewrote, so the eye is
   sent to the right place in a six-section document. Once, then gone — a
   permanent tint would still be saying "new" tomorrow, which is how a
   highlight stops meaning anything. On the block, not the body: a design
   owns the body's background and would fight it. */
/* ---------- The changed lines ----------
   Marked one line at a time, each from the moment it arrives (the section
   streams over itself — see handleBlockDelta), and only where the new line
   differs from the one it replaced. So a one-line edit lights one line, and
   a full rewrite lights the section because that is what happened.

   Drawn as a pseudo-element BEHIND the text rather than as a background: the
   design owns the block's background and would fight a background here, and
   the element itself is inside a contenteditable region where a wrapper
   would become something a keystroke can break. z-index keeps it under the
   words, inset bleeds it into the gutter so it reads as a highlighter pass
   rather than a box. */
/* `isolation` is doing real work: it gives the line its own stacking
   context, which is what keeps the ::before below from escaping into the
   parent's. Without it a z-index of -1 paints the mark behind the BLOCK's
   background — and the design paints that background — so the highlight was
   invisible in every real document and showed only in the moment before a
   design applied. */
.block-body .line-revised {
  position: relative;
  isolation: isolate;
}
.block-body .line-revised::before {
  content: '';
  position: absolute;
  inset: -3px -8px;
  border-radius: 5px;
  pointer-events: none;
  z-index: -1;
  background: color-mix(in srgb, var(--ui-accent) 15%, transparent);
  animation: revised-in .4s ease-out 1 both;
  transition: opacity .5s ease;
}
/* It STAYS until the next click in the document. Two timed fades were tried
   first — 1.6s, then 4.5s — and both are a guess about when the reader will
   look, which is unknowable: the page deliberately does not jump to the
   change, so they may be reading somewhere else entirely. A mark that waits
   needs no guess, and dismissing it is a click they were going to make. */
@keyframes revised-in { from { opacity: 0; } }
.block-body .line-revised.dismissing::before { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .block-body .line-revised::before { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .receipt { animation: none; }
}

/* A PERSON's note, not an agent's question.
   The two sit in the same lane and looked identical, so the reader's own
   words came back at them wearing an agent's badge. The avatar carries the
   identity; this carries the kind — a person's pin takes the app's accent
   rather than the agent-question colour, so a lane holding both can be read
   at a glance without opening either. */
.comment-wrap.from-person .comment-dot {
  background: var(--ui-accent);
  border-radius: 50% 50% 6px 50%; /* mirrored from the agent teardrop */
}

/* ---------- Komentar (Figma-style comment pins) ---------- */
/* The lane lives OUTSIDE the document, on the right — the mirror of the left
   rail, which carries what the shell has to say while this carries what
   people say. Pins sat on the text's right edge before, inside the block box.
   Only the PINS moved: the card is 320px and a margin that could hold it
   would have to be wider than the space there is, so it still opens inward
   over the text (`.comment` is anchored to the wrap's right edge below).
   layOutPins publishes --pin-top and --pin-x; which side they are read from
   is this file's business, so one layout function serves both. */
.comment-wrap {
  position: absolute;
  top: var(--pin-top, 8px);
  left: calc(100% + 22px + var(--pin-x, 0px));
  z-index: 5;
}

/* Under the section instead, once there is no margin to put them in. Same
   pins, same cards, in normal flow after the block — a lane squeezed against
   the viewport edge would be worse than one that simply moves. */
/* 1000, not the left rail's 1200. The two rails need different amounts of
   room and should not share a number: the left one carries wrapped TEXT and
   wants ~190px, while this one carries a 30px pin. At a 1000px viewport a
   760px document still leaves 120px a side, which a pin fits in comfortably
   and a status line does not. */
@media (max-width: 1000px) {
  /* Not the ENTRY box: it opens on hovering the +, and stays open only while
     the pointer is over it, so it has to sit where the button is. In flow it
     would appear under the section while the cursor is up on the button, and
     the box would be created invisible — the same trap as the hidden dot. */
  .comment-wrap:not(.entry) {
    position: relative;
    top: auto;
    left: auto;
    display: inline-flex;
    vertical-align: top;
    margin: 0 8px 8px 0;
    z-index: 5;
  }
  /* The + stays in the block's own corner here. In flow it would come BEFORE
     the pins — it is created with the block, they arrive later — so "beside
     the last comment" would read as "before the first". */
  .comment-wrap.entry,
  .block-comment-button {
    left: auto;
    right: 6px;
    top: 8px;
  }
  /* In flow the pins read left-to-right, so a card anchored to its wrap's
     RIGHT edge would hang off the left of the first one. */
  .comment-wrap:not(.entry) .comment { right: auto; left: 0; }
}
.comment-dot {
  width: 30px; height: 30px;
  border-radius: 50% 50% 50% 6px;
  border: 2px solid #fff;
  background: var(--ui-comment);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--ui-shadow-soft);
  padding: 0; overflow: hidden;
  animation: pop-in .3s cubic-bezier(.34,1.56,.64,1);
  transition: transform .15s;
}
.comment-dot img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; border-radius: inherit;
}
/* The dot as a conversation: the last speaker in front, the previous voice
   peeking from beneath. Overflow must be visible for the peek to exist. */
.comment-dot:has(.dot-top) { overflow: visible; position: relative; }
.comment-dot .dot-top, .comment-dot .dot-peek {
  position: absolute; inset: -2px;
  border-radius: inherit; overflow: hidden;
  border: 2px solid #fff; background: var(--ui-comment);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
}
.comment-dot .dot-peek {
  transform: translate(7px, 9px) scale(.68);
  z-index: -1;
  box-shadow: var(--ui-shadow-soft);
}
.comment-dot .dot-top img, .comment-dot .dot-peek img,
.comment-dot .dot-top .comment-user-avatar, .comment-dot .dot-peek .comment-user-avatar,
.comment-dot .dot-top .comment-avatar, .comment-dot .dot-peek .comment-avatar {
  width: 100%; height: 100%; border-radius: inherit;
  object-fit: cover; margin: 0; font-size: 12px;
  display: flex; align-items: center; justify-content: center;
  outline: none;
}
.comment-dot:hover { transform: scale(1.08); }
@keyframes pop-in { from { transform: scale(0); } }
/* A settled thread is history, not something to look at. It shrinks to a
   quiet tick, and the resolved ones nest sideways in a single row at the top
   of the lane — so a block's whole history costs one row, whatever its
   length. Hovering brings one forward; they stay readable, just not loud. */
.comment-wrap.resolved .comment-dot {
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 1.5px solid var(--ui-surface);
  background: color-mix(in srgb, var(--ui-accent) 16%, var(--ui-surface));
  color: var(--ui-accent);
  box-shadow: 0 1px 2px rgba(0,0,0,.10);
  font-size: 0;
}
/* The tick is drawn, not typed: the avatar image inside is replaced by it. */
.comment-wrap.resolved .comment-dot img,
.comment-wrap.resolved .comment-dot .ms-mark { display: none; }
.comment-wrap.resolved .comment-dot::after {
  content: '';
  width: 8px; height: 4px;
  border: 1.6px solid currentColor;
  border-top: 0; border-right: 0;
  transform: rotate(-45deg) translate(1px, -1px);
}
.comment-wrap.resolved:hover { z-index: 40 !important; }
.comment-wrap.resolved:hover .comment-dot { transform: scale(1.15); }
/* The thread itself hangs below the small marker, not the full-size one. */
.comment-wrap.resolved .comment { top: 26px; }
/* Nested ticks sit further from the edge, so their thread would otherwise
   open off-centre; pin it back to the lane. */
.comment-wrap.resolved .comment { right: 0; }
.comment {
  display: block;
  position: absolute;
  top: 38px; right: 0;
  width: min(320px, 84vw);
  padding: 16px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  border: 1px solid var(--ui-glass-edge);
  outline: 1px solid var(--ui-glass-edge-dark); outline-offset: -1px;
  border-radius: 16px;
  font-size: 13.5px;
  box-shadow: var(--ui-glass-shadow);
  max-height: min(520px, 70vh); overflow-y: auto;
  overscroll-behavior: contain;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  /* Closing waits long enough to cross the gap from pin to card. Visibility
     stays live through the fade so entering the card cancels the close. */
  transition:
    opacity .14s ease .2s,
    transform .14s ease .2s,
    visibility 0s linear .34s;
}
.comment-wrap:hover .comment {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition-delay: .3s, .3s, .3s;
}
.comment-wrap:focus-within .comment,
.comment-wrap.mentioning .comment,
.comment-wrap.pinned .comment {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition-delay: 0s;
}
/* An open card must cover the NEXT block's pin, which is a later sibling at
   the same z-index and would otherwise paint through the card. And a card
   someone is USING must cover the composer too (z-index 50): near the bottom
   of the viewport the reply input and the mention picker were painted under
   the prompt bar, which read as "mentions don't work" — the list was fine,
   the field being filled was invisible. Modals (60+) stay above. */
.comment-wrap:hover, .comment-wrap:focus-within, .comment-wrap.pinned,
.comment-wrap.entry, .comment-wrap.mentioning { z-index: 55; }
/* A colleague's staged comment: real pin, read-only card, clearly a draft. */
.comment-wrap.colleague .comment-dot { border: 1.5px dashed var(--ui-line); }
.colleague-draft { font-weight: 400; font-size: 11px; color: var(--ui-glass-text); }
.comment-avatar-letter {
  display: inline-grid; place-items: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--ui-line); color: var(--ui-ink);
  font: 600 13px var(--ui-font);
}

/* The owner's resolve check on an agent thread. Hidden for everyone else:
   closing a thread is a judgement about the document, and it has one owner.
   Quiet on purpose — a faint check that only becomes a control under the
   pointer; anything heavier reads as a primary action on every card. */
/* Sized like the one in Docs: the thread's primary action, legible at a
   glance rather than a decoration you have to hunt for. */
.comment-message .comment-resolve {
  display: none;
  width: 30px; height: 30px; flex: 0 0 auto; padding: 0;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--ui-glass-text);
  cursor: pointer;
  align-items: center; justify-content: center;
  transition: background .12s ease, color .12s ease;
}
body.role-owner .comment-message .comment-resolve { display: inline-flex; }
.comment-message .comment-resolve svg { width: 20px; height: 20px; display: block; }
.comment-message .comment-resolve:hover,
.comment-message .comment-resolve:focus-visible {
  background: color-mix(in srgb, var(--ui-accent) 14%, transparent);
  color: var(--ui-accent);
  outline: none;
}
.comment-message .comment-resolve:active { transform: scale(.92); }
.comment-wrap.resolved .comment-resolve { display: none !important; }

/* A colleague's reply inside a thread: same row as any message, slightly
   set off so "already part of the thread" and "still theirs to send" both
   read at a glance. */
.colleague-reply { opacity: .85; }
.colleague-reply .comment-avatar { outline: 1.5px dashed var(--ui-line); outline-offset: 1px; }

.comment-message {
  /* Third track: the owner's resolve check on agent messages; empty otherwise. */
  display: grid; grid-template-columns: 28px minmax(0, 1fr) auto;
  gap: 10px; align-items: start;
}
.comment-avatar, .comment-user-avatar {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 8px;
  object-fit: cover; flex: 0 0 auto;
}
.comment-user-avatar {
  background: var(--ui-ink); color: #fff;
  font-size: 11px; font-weight: 700;
}

/* ---------- The madSense mark ----------
   The analyst's face in the embedded build: madSense's nested-squares glyph
   alone, on a disc of the surface colour, in their teal. It used to be their
   favicon — a white TILE with the glyph on it — loaded as an <img>: inside a
   20–30px circle the tile's corners were clipped away and the glyph sat
   cramped against the edge. It is drawn here instead of shipped as a picture,
   so the glyph takes its colour from the chrome (--ui-mark: teal in light, a
   lighter teal in dark) and follows a theme change live, which no <img> can.

   The element carries the same class as the face it stands in for
   (.comment-avatar, .block-agent-face, the pin's face), so every size, ring
   and shape rule that already governs that slot governs it; border-radius:
   inherit takes the slot's own shape, a pin's teardrop included. The glyph is
   the SVG file used as a MASK over a coloured box — the file's own fill never
   shows — at 60% of the disc, which is where a nested-squares figure stops
   reading as a border and starts reading as a mark. app.js agentFaceHtml /
   agentFaceEl produce it. */
.ms-mark {
  position: relative;
  display: inline-block;
  background: var(--ui-surface);
  border-radius: 50%;
  overflow: hidden;
  flex: 0 0 auto;
  color: transparent; /* never an initial */
}
/* Placed by insets rather than sized by percentages: inside the comment
   card's grid a percentage width on this box resolved to nothing, and the
   disc came up empty. Insets resolve against the disc, wherever it sits. */
.ms-mark::before {
  content: '';
  position: absolute; inset: 20%;
  background: var(--ui-mark, #0c8578);
  -webkit-mask: url(madsense-mark.svg) center / contain no-repeat;
  mask: url(madsense-mark.svg) center / contain no-repeat;
}
/* Where the face fills a slot sized and shaped by its parent — the pin, with
   its teardrop, and the team row — rather than by its own class. The parent's
   own border is its ring. */
.comment-dot .ms-mark, .team-member .ms-mark {
  width: 100%; height: 100%;
  border-radius: inherit; box-shadow: none;
}
/* In the comment card a person's picture is a rounded square; the mark is a
   badge, so a disc, with a hairline where a white disc would otherwise
   dissolve into the white card. */
.comment-avatar.ms-mark { box-shadow: 0 0 0 1px var(--ui-line); }
.comment-dot .comment-avatar.ms-mark { border-radius: inherit; box-shadow: none; }

.comment-message-body { min-width: 0; }
.comment .author {
  font-weight: 650; font-size: 12px; line-height: 1.25;
  letter-spacing: 0; color: var(--ui-comment); margin-bottom: 3px;
}
.comment-text { color: var(--ui-ink); line-height: 1.45; overflow-wrap: anywhere; }
.comment-replies:not(:empty) {
  display: flex; flex-direction: column; gap: 14px;
  margin-top: 14px; padding-top: 14px;
  border-top: 1px solid rgba(0,0,0,.08);
}
/* Bottom-aligned: the field grows downward as someone writes, and the send
   button should stay level with its last line rather than float mid-field. */
.comment .answer-row { display: flex; gap: 8px; margin-top: 12px; align-items: flex-end; }
.comment textarea {
  flex: 1; min-width: 0; border: 1px solid var(--ui-line); border-radius: 16px;
  padding: 8px 14px; font-size: 13.5px; line-height: 1.45;
  font-family: inherit;
  background: var(--ui-background); outline: none;
  /* Grown in JS to at most four lines, then it scrolls; the handle is
     removed because a card that floats over the document should not be
     resizable into covering it. */
  resize: none; overflow-y: auto;
  transition: border-color .15s, background .15s;
}
.comment textarea:focus { border-color: var(--ui-comment); background: #fff; }
/* The passage the comment is about. Shown, not typed: it cannot be edited
   into a misquote, and the field stays empty for what they came to say. */
.comment-quote {
  margin-top: 10px;
  padding: 6px 10px;
  border-left: 2px solid var(--ui-line);
  font-size: 12.5px; line-height: 1.45;
  color: var(--ui-gray);
  max-height: 3.6em; overflow: hidden;
}
.comment button {
  border: none; background: var(--ui-ink); color: #fff;
  border-radius: 999px; padding: 8px 16px; font-size: 13px;
  font-weight: 600; cursor: pointer; transition: opacity .15s;
}
.comment button:hover { opacity: .85; }
.comment-wrap.resolved .answer-row { display: none; }
.comment-wrap.resolved .comment-text { color: var(--ui-gray); }

/* Design progress lives on the Design control; these legacy nodes remain
   in the DOM for the existing event contract but have no visual surface. */
.design-bar { display: none; }

/* ---------- QA nit (answers about the document, inside the pill) ---------- */
/* The composer expands well beyond the status stack: the thread sits above
   the input row, scrolls internally, and is capped to a slice of the
   screen. It collapses on send and reopens only when a new answer lands. */
/* The outer box owns the animated height and the single shared separator;
   the inner box carries only content padding. The pill grows upward —
   nothing in the page reflows. */
.qa-thread {
  height: 0; overflow: hidden;
  margin: 0 64px 0 8px;
  border-bottom: 0 solid rgba(0,0,0,.10);
  transition: height .5s cubic-bezier(.22,1,.36,1);
  will-change: height;
}
.composer form.qa-open .qa-thread { border-bottom-width: 1px; }
.qa-thread-inner {
  padding: 6px 8px 12px;
}
.qa-pair { padding: 8px 0 2px; }
.qa-pair + .qa-pair { border-top: 1px solid rgba(0,0,0,.06); }
.qa-question {
  margin: 0 0 5px;
  font: italic 13px/1.4 'Iowan Old Style', 'Palatino', Georgia, serif;
  color: var(--ui-gray);
}
.qa-answer { font-size: 14px; line-height: 1.55; color: var(--ui-ink); }
.qa-answer p { margin: 0 0 8px; }
.qa-answer p:last-child { margin-bottom: 0; }
.qa-answer ul, .qa-answer ol { margin: 0 0 8px; padding-left: 18px; }
.qa-answer table { width: 100%; border-collapse: collapse; margin: 6px 0 8px; font-size: 13px; }
.qa-answer th, .qa-answer td { text-align: left; padding: 4px 8px; border-bottom: 1px solid rgba(0,0,0,.08); }
.qa-answer code { background: rgba(0,0,0,.06); border-radius: 4px; padding: 1px 5px; font-size: 12.5px; }

/* The connect offer: the composer's answer when someone asks for a document
   about their own account data and nothing is connected that could supply it.
   Styled as the Q&A thread's own voice with one action row, NOT as a banner —
   it is an offer with an escape hatch, and a modal-looking thing would read as
   a wall in front of the product.

   Scoped under .composer deliberately: `.composer button` makes every button
   in there the send key — a 42px black circle — which clipped these two into
   dark coins with a word cut in half. Beating that rule needs the same
   ancestor, and the size properties have to be named to be undone. */
.composer .connect-offer-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  flex-wrap: wrap;
}
.composer .connect-offer-go,
.composer .connect-offer-skip {
  width: auto;
  height: auto;
  border-radius: 999px;
  padding: 7px 15px;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease;
}
.composer .connect-offer-go {
  background: var(--ui-ink);
  color: #fff;
  border: 1px solid var(--ui-ink);
}
.composer .connect-offer-skip {
  background: transparent;
  color: var(--ui-gray);
  border: 1px solid var(--ui-line);
}
/* The send key grows on hover; a pill that does the same reads as wobbling. */
.composer .connect-offer-go:hover { opacity: 0.86; transform: none; }
.composer .connect-offer-skip:hover {
  opacity: 1;
  color: var(--ui-ink);
  background: transparent;
  transform: none;
}
/* The same-tab route, offered after Meta's popup failed to arrive. It leads
   the row because it is the path that has not already been tried. */
.composer .connect-offer-tab { order: -1; }
.composer .connect-offer-go:disabled,
.composer .connect-offer-skip:disabled {
  opacity: 0.5;
  cursor: default;
  transform: none;
}

/* ---------- Status linija (one line above the input) ---------- */
/* Same height-tween mechanism as the Q&A thread: the outer box owns the
   animated height and separator, the inner box owns content padding.
   Messages replace each other with a crossfade;
   the line stays until the user clicks away or hides it. */
.status-area {
  height: 0; overflow: hidden;
  margin: 0 64px 0 8px;
  border-bottom: 0 solid rgba(0,0,0,.10);
  transition: height .42s cubic-bezier(.22,1,.36,1);
}
.composer form.status-open .status-area { border-bottom-width: 1px; }
.status-inner {
  display: flex; align-items: center; gap: 8px;
  /* No vertical margins: offsetHeight (the measured tween target) ignores
     them, so with overflow:hidden the bottom border was being clipped. */
  padding: 8px 4px 10px 8px;
}
/* "View" — offered after a revision, never taken automatically. The page
   does not jump to the change (the reader was somewhere for a reason), so
   this is the trip made available rather than made. Flex-none beside the
   ellipsised message, so a long status line shortens and the action does
   not.

   Scoped under .composer, and every one of these properties is undoing
   `.composer button`: that rule is the SEND button's, a 42px black circle,
   and it wins on specificity over a bare class. A quiet pill is what belongs
   beside a status line — the send button is the loud thing in this row and
   there should only be one. */
.composer .status-view {
  flex: none;
  width: auto; height: auto;
  border: 0;
  border-radius: 999px;
  padding: 4px 12px;
  font-family: inherit;
  font-size: 12.5px; font-weight: 600; line-height: 1.25;
  background: color-mix(in srgb, var(--ui-accent) 13%, transparent);
  color: var(--ui-accent);
  cursor: pointer;
}
.composer .status-view:hover {
  background: color-mix(in srgb, var(--ui-accent) 22%, transparent);
  opacity: 1;
  transform: none; /* .composer button:hover scales; a text pill must not */
}
.composer .status-view[hidden] { display: none; }
.status-text {
  flex: 1; min-width: 0;
  font-size: 13px; line-height: 1.4; color: var(--ui-glass-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: opacity .16s ease, color .16s ease;
}
.status-text.success { color: #1a7f4b; }   /* dark green */
.status-text.error { color: #a33; }
/* ---------- Bok (sidecar on the left) ---------- */
.sidecar-button {
  position: fixed; top: 20px; left: 20px; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  border: 1px solid var(--ui-glass-edge); border-radius: 50%;
  background: var(--ui-glass); color: var(--ui-ink);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  cursor: pointer; box-shadow: var(--ui-glass-shadow);
  transition: opacity .15s, transform .15s;
}
.sidecar-button:hover { opacity: .85; transform: scale(1.05); }
.sidecar-button svg { display: block; }
/* Open: the toggle stops being a floating pill and becomes part of the
   panel — a flat icon in its header (same spot, no surface of its own). */
.sidecar-button.open {
  background: transparent; border-color: transparent; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.sidecar-button.open:hover { transform: none; opacity: 1; background: transparent; }
.sidecar {
  position: fixed; top: 0; left: 0; bottom: 0; z-index: 55;
  width: 272px;
  padding: 72px 14px 24px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  box-shadow: 1px 0 0 rgba(0,0,0,.06), 12px 0 40px rgba(0,0,0,.08);
  transform: translateX(-100%);
  transition: transform .38s cubic-bezier(.22,1,.36,1);
  overflow-y: auto;
  /* Column so the account block can sit at the bottom (margin-top:auto). */
  display: flex; flex-direction: column;
}
.sidecar.open { transform: translateX(0); }
.sidecar-menu { display: flex; flex-direction: column; gap: 18px; }
.sidecar-group { display: flex; flex-direction: column; }
/* Author rules outrank the browser's own [hidden]{display:none}, so the line
   above silently cancelled the attribute: a section hideEmbeddedSidecarSections()
   had set hidden went on rendering, heading, rows and "See all". */
.sidecar-group[hidden] { display: none; }
.sidecar-heading {
  margin: 0 0 4px 12px;
  font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--ui-gray);
}
.sidecar-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.sidecar-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 12px; border: 0; border-radius: 10px;
  background: none; color: var(--ui-ink); text-align: left;
  font: 500 13.5px/1.3 var(--ui-font); cursor: pointer;
  transition: background .12s;
  min-width: 0;
}
.sidecar-item:hover { background: rgba(0,0,0,.05); }
.sidecar-item.active { background: rgba(0,0,0,.07); }
.sidecar-item .sidecar-text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.sidecar-item .sidecar-face { width: 18px; height: 18px; border-radius: 50%; flex: 0 0 auto; object-fit: cover; }
.sidecar-item-icon { font-size: 16px; line-height: 1; width: 16px; text-align: center; }
.sidecar-new { font-weight: 600; }
.sidecar-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ui-dot, #999); flex: 0 0 auto; }
.sidecar-agent-avatar {
  width: 18px; height: 18px; flex: 0 0 auto;
  border-radius: 5px; object-fit: cover;
}
/* Stands in for a portrait on agents that do not have one, at exactly the
   avatar's footprint so the column of faces stays aligned. */
.sidecar-agent-mark {
  width: 18px; height: 18px; flex: 0 0 auto;
  border-radius: 5px; background: var(--ui-dot, #666); color: #fff;
  display: grid; place-items: center;
  font-size: 10px; font-weight: 700; letter-spacing: 0;
}
.sidecar-note { margin-left: auto; font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--ui-gray); }
/* A connected source names the account it is linked to, which is a proper
   noun rather than a status word — so it drops the uppercasing, and the dot
   takes the source's own colour at full strength to read as "live". */
.sidecar-item.connected .sidecar-note {
  text-transform: none; letter-spacing: 0; font-weight: 500;
  color: var(--ui-ink); opacity: .75;
  max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidecar-item.connected .sidecar-dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-dot, #999) 22%, transparent); }
.sidecar-empty { padding: 6px 12px; font-size: 12.5px; line-height: 1.4; color: var(--ui-gray); }
/* A project row is two lines: the client, and the report it last produced.
   Stacked rather than name-plus-right-aligned-note, because a report title
   is a sentence and would have ellipsised down to nothing beside a name. */
.sidecar-item-stack { flex-direction: column; align-items: stretch; gap: 1px; }
/* The note's colour and weight without its uppercasing: a report's title is
   a proper noun, and small caps make one unreadable. */
.sidecar-sub {
  font-size: 11px; font-weight: 500; color: var(--ui-gray);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.sidecar-all {
  align-self: flex-start; margin: 2px 0 0 12px; padding: 4px 0;
  border: 0; background: none; color: var(--ui-gray);
  font: 500 12.5px var(--ui-font); cursor: pointer;
}
.sidecar-all:hover { color: var(--ui-ink); }

/* ---------- Specs gallery ---------- */
.doc.spec-gallery-doc {
  max-width: 760px;
  padding-top: 28px;
}
.spec-gallery-page h1 {
  margin: 0 0 18px;
  font-size: 28px; line-height: 1.1; letter-spacing: 0;
}
.spec-gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 20px 14px;
}
.spec-card {
  /* The "…" menu and the panel it opens are positioned against the card. */
  position: relative;
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  min-width: 0; padding: 0;
  border: 0; border-radius: 8px;
  background: none; color: var(--ui-ink);
  text-align: left; cursor: pointer;
  transition: transform .15s;
}
.spec-card:hover, .spec-card:focus-visible { transform: translateY(-1px); outline: none; }
.spec-card:hover .spec-thumb, .spec-card:focus-visible .spec-thumb {
  border-color: rgba(0,0,0,.18);
  box-shadow: var(--ui-shadow-float);
}
.spec-card.active-f .spec-thumb { border-color: var(--ui-ink); box-shadow: inset 0 0 0 1px var(--ui-ink), var(--ui-shadow-soft); }
.spec-thumb {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border: 1px solid var(--ui-line);
  border-radius: 8px;
  background: #fff;
  box-shadow: var(--ui-shadow-soft);
  transition: border-color .15s, box-shadow .15s;
}
.spec-thumb iframe {
  position: absolute; left: 0; top: 0;
  width: 760px; height: 980px;
  border: 0;
  transform: scale(.31);
  transform-origin: 0 0;
  pointer-events: none;
}
/* A preview that could not load stays an empty sheet rather than showing
   whatever the server said. */
.spec-thumb.empty { background: var(--ui-paper, #fff); }
.spec-card-title {
  padding: 0 2px;
  font-size: 13px; font-weight: 600; line-height: 1.35; color: var(--ui-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.spec-card-meta {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 0 2px;
  font-size: 11px; font-weight: 600; color: var(--ui-gray);
}
.spec-card-count { white-space: nowrap; }
.spec-gallery-empty {
  grid-column: 1 / -1;
  padding: 22px;
  border: 1px dashed var(--ui-line); border-radius: 8px;
  color: var(--ui-gray); text-align: center;
}
.connector-card {
  display: flex; flex-direction: column; align-items: flex-start;
  min-height: 190px; padding: 14px;
  border: 1px solid var(--ui-line); border-radius: 8px;
  background: #fff; color: var(--ui-ink);
  box-shadow: var(--ui-shadow-soft);
}
.connector-badge, .connector-plus {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px;
  border-radius: 8px;
  background: var(--ui-color, var(--ui-ink)); color: #fff;
  font-size: 15px; font-weight: 800; line-height: 1;
}
.connector-badge.has-avatar {
  width: 52px; height: 52px;
  padding: 0; overflow: hidden; background: none;
}
.connector-badge.has-avatar img {
  display: block; width: 100%; height: 100%; object-fit: cover;
}
.connector-plus {
  background: rgba(0,0,0,.06); color: var(--ui-ink);
  font-size: 22px; font-weight: 600;
}
.connector-title {
  display: block;
  margin-top: 14px;
  font-size: 15.5px; font-weight: 750; line-height: 1.15; letter-spacing: 0;
}
.connector-description {
  display: block;
  margin-top: 7px;
  min-height: 50px;
  font-size: 12.5px; font-weight: 500; line-height: 1.35; color: var(--ui-gray);
}
.connector-button, .connector-actions button {
  margin-top: auto;
  border: 0; border-radius: 999px;
  background: var(--ui-ink); color: #fff;
  padding: 7px 12px;
  font-size: 12px; font-weight: 700;
  cursor: pointer;
}
.connector-button:hover, .connector-actions button:hover { opacity: .86; }
.connector-button:disabled {
  background: rgba(15,118,110,.1); color: #0f766e;
  cursor: default; opacity: 1;
}
.connector-card.available { border-color: rgba(15,118,110,.22); }

/* Not built yet. Muted rather than disabled-looking: these are a catalogue of
   what is coming, and the card should still read as an invitation — it just
   must not pretend to be a control. */
.connector-card.announced { background: #fcfcfd; border-style: dashed; }

/* The agent switch. A control, not a badge: the knob slides, the track
   fills, and a locked switch shows its on-state without inviting a click. */
.agent-switch {
  margin-top: auto;
  width: 40px; height: 22px; padding: 0;
  border: 0; border-radius: 999px;
  background: #d5d5da;
  position: relative; cursor: pointer;
  transition: background .18s ease;
}
.agent-switch .agent-switch-knob {
  position: absolute; top: 2px; left: 2px;
  width: 18px; height: 18px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25);
  transition: transform .18s ease;
}
.agent-switch.on { background: #0f766e; }
.agent-switch.on .agent-switch-knob { transform: translateX(18px); }
.agent-switch:disabled { cursor: default; opacity: .55; }
.connector-card.announced .connector-badge { opacity: .45; }
.connector-card.announced .connector-title { color: var(--ui-gray); }
.connector-button:disabled.connector-soon,
.connector-actions button:disabled {
  background: transparent; color: var(--ui-gray);
  box-shadow: inset 0 0 0 1px var(--ui-line);
}
.connector-actions { display: flex; gap: 8px; margin-top: auto; }
.connector-actions button {
  margin-top: 0;
  min-width: 52px;
}
.connector-add {
  border-style: dashed;
  background: rgba(255,255,255,.64);
}
@media (max-width: 700px) {
  .doc.spec-gallery-doc { max-width: 430px; }
  .spec-gallery-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  .spec-thumb iframe { transform: scale(.26); }
}

/* ---------- Design (design-change affordance, popover styled like .comment) ---------- */
.design-button {
  display: none;
  align-items: center; justify-content: center;
  /* Icon-only: a circle, not the wide pill this was when it had a label. */
  width: 36px; height: 36px; padding: 0;
  border: 1px solid var(--ui-glass-edge); border-radius: 50%;
  background: var(--ui-glass); color: var(--ui-ink);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  font-size: 14px; font-weight: 600; cursor: pointer;
  box-shadow: var(--ui-glass-shadow);
  transition: opacity .15s, transform .15s;
}
.design-button { display: flex; } /* always available — design is set before the first prompt too */
.design-button:hover { opacity: .85; transform: scale(1.05); }
.design-button svg { display: block; }
.design-button::after {
  content: '';
  position: absolute; inset: -4px;
  padding: 2px;
  border-radius: 999px;
  background: conic-gradient(var(--ui-ink) 0 var(--ui-design-progress, 0%), rgba(24,24,27,.12) 0 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0; pointer-events: none;
  transition: opacity .18s ease, --ui-design-progress 4s linear;
}
.design-button::before {
  content: 'Design in progress';
  position: absolute; top: calc(100% + 10px); right: 0;
  width: max-content;
  padding: 6px 8px;
  border-radius: 6px;
  background: var(--ui-ink); color: #fff;
  font: 600 11px/1.25 var(--ui-font);
  letter-spacing: 0;
  box-shadow: var(--ui-shadow-float);
  opacity: 0; visibility: hidden; transform: translateY(-2px);
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
}
.design-button.designing::after {
  opacity: 1;
}
.design-button.design-ready::after {
  opacity: 1;
  transition-duration: .18s, .7s;
}
.design-button.designing::before {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition-delay: .35s;
}
.design-button.design-error { color: #a33; }
.design-button.design-error::after {
  opacity: 1;
  background: #a33;
}
@property --ui-design-progress {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 0%;
}
.design-popover {
  display: none;
  position: fixed; top: 64px; right: 20px; z-index: 60;
  width: min(320px, 84vw);
  padding: 16px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  border: 1px solid var(--ui-glass-edge);
  outline: 1px solid var(--ui-glass-edge-dark); outline-offset: -1px;
  border-radius: 16px;
  font-size: 13.5px;
  box-shadow: var(--ui-glass-shadow);
}
.design-popover.open { display: block; }
.design-header { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin: 2px 0 12px; }
.design-title { font-weight: 700; font-size: 13px; letter-spacing: .01em; color: var(--ui-ink); }
/* Effort slider: five stops, the label above tracks the thumb. */
.design-effort { margin-bottom: 4px; }
.design-effort-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.design-effort-label { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ui-gray); }
.design-effort-value { font-size: 12.5px; font-weight: 600; color: var(--ui-ink); }
.design-popover input[type=range] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 20px; margin: 0; padding: 0; border: 0; background: none; outline: none;
}
.design-popover input[type=range]::-webkit-slider-runnable-track {
  height: 20px; border-radius: 10px;
  background: linear-gradient(90deg, var(--ui-ink) var(--ui-fill, 25%), rgba(0,0,0,.12) var(--ui-fill, 25%));
}
.design-popover input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 20px; height: 20px; margin-top: 0; border-radius: 50%;
  background: #fff; border: none;
  box-shadow: 0 0 0 1.5px rgba(0,0,0,.18), 0 1px 3px rgba(0,0,0,.25); cursor: grab;
  transition: transform .15s;
}
.design-popover input[type=range]:active::-webkit-slider-thumb { transform: scale(1.12); cursor: grabbing; }
.design-popover input[type=range]::-moz-range-track { height: 20px; border-radius: 10px; background: rgba(0,0,0,.12); }
.design-popover input[type=range]::-moz-range-progress { height: 20px; border-radius: 10px; background: var(--ui-ink); }
.design-popover input[type=range]::-moz-range-thumb {
  width: 20px; height: 20px; border-radius: 50%; background: #fff; border: none;
  box-shadow: 0 0 0 1.5px rgba(0,0,0,.18), 0 1px 3px rgba(0,0,0,.25); cursor: grab;
}
.design-effort-scale { display: flex; justify-content: space-between; margin-top: 2px; padding: 0 2px; }
.design-effort-scale span { font-size: 10px; color: var(--ui-gray); letter-spacing: .02em; }
/* The look prompt appears beneath the slider once a design exists. */
.design-form { display: none; margin-top: 14px; padding-top: 14px; border-top: 1px solid rgba(0,0,0,.07); }
.design-popover.has-design .design-form { display: block; }
.design-popover .row { display: flex; gap: 8px; }
.design-popover input {
  flex: 1; min-width: 0; border: 1px solid var(--ui-line); border-radius: 999px;
  padding: 8px 14px; font-size: 13.5px; background: var(--ui-background); outline: none;
  transition: border-color .15s, background .15s;
}
.design-popover input:focus { border-color: var(--ui-comment); background: #fff; }
.design-popover button {
  border: none; background: var(--ui-ink); color: #fff;
  border-radius: 999px; padding: 8px 16px; font-size: 13px;
  font-weight: 600; cursor: pointer; transition: opacity .15s;
}
.design-popover button:hover { opacity: .85; }


/* ---------- Prompt bar ---------- */
.composer {
  position: fixed; left: 0; right: 0; bottom: 0;
  padding: 14px 20px 20px;
  /* No light-colored fade — the bar floats over whatever the theme paints,
     above the activity scrim. */
  display: flex; justify-content: center;
  z-index: 50;
}
.composer form {
  position: relative;
  max-width: 760px; width: 100%;
  display: flex; flex-direction: column; align-items: stretch;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  border: 1px solid var(--ui-glass-edge);
  outline: 1px solid var(--ui-glass-edge-dark); outline-offset: -1px;
  border-radius: 27px; /* pill when collapsed (54px row), soft rect when grown */
  padding: 6px 6px 6px 8px;
  box-shadow: var(--ui-glass-shadow);
  transition: border-color .15s, padding .4s cubic-bezier(.22,1,.36,1);
}
.composer form:focus-within { outline-color: rgba(0,0,0,.12); }
.composer form.accepting-file {
  outline: 2px solid rgba(0,0,0,.32);
  outline-offset: 2px;
}
.input-row { display: flex; gap: 8px; align-items: center; height: 42px; }

/* ---------- Context tabs (comments + selections): small browser-style
   tabs sitting ON the input pill's top edge. Click = scroll to the source;
   ✕ = unstage. Answered/active = black, waiting = gray. */
.context-strip {
  display: none;
  position: absolute; bottom: 100%; left: 20px; right: 64px;
  /* One row, browser-tab style: tabs shrink to their icon+✕ floor, and
     beyond that the strip scrolls horizontally (scrollbar hidden). */
  flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
  gap: 4px; align-items: flex-end;
  scrollbar-width: none;
}
.context-strip::-webkit-scrollbar { display: none; }
.context-strip.has-items { display: flex; }
.context-hint {
  position: fixed; z-index: 55;
  width: max-content; max-width: min(220px, calc(100vw - 16px));
  padding: 7px 10px;
  border-radius: 7px;
  background: var(--ui-ink); color: #fff;
  font: 600 11.5px/1.35 var(--ui-font);
  letter-spacing: 0;
  box-shadow: var(--ui-shadow-float);
  opacity: 0; visibility: hidden; transform: translateY(3px);
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
}
.context-hint::after {
  content: '';
  position: absolute; left: var(--ui-hint-top, 50%); bottom: -4px;
  width: 8px; height: 8px;
  background: var(--ui-ink);
  transform: translateX(-50%) rotate(45deg);
}
.context-hint.shown {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition-delay: 0s;
}
.chip {
  display: inline-flex; align-items: center; gap: 4px;
  /* min-width:auto = the icon + ✕ are the shrink floor; only text truncates */
  flex: 0 1 auto; max-width: 170px;
  height: 24px;
  padding: 0 6px 0 10px;
  font-family: var(--ui-font);
  font-size: 11px; font-weight: 600; line-height: 1;
  /* Chips sit directly over running text (they hang above the pill, on the
     page): nearly solid, so the line behind never reads through them. */
  background: rgba(226,226,230,.92); color: var(--ui-gray);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  border: 1px solid var(--ui-glass-edge); border-bottom: none;
  border-radius: 8px 8px 0 0;
  cursor: pointer; user-select: none;
  transition: background .15s, color .15s;
}
.chip:hover { background: rgba(214,214,219,.96); }
.chip .chip-text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.chip .chip-mark { font-weight: 700; flex-shrink: 0; }
.chip.answered, .chip.selection, .chip.attachment {
  background: var(--ui-ink); color: #fff; border-color: var(--ui-ink);
  /* a faint light ring keeps an ink chip visible over a dark section */
  box-shadow: 0 0 0 1px rgba(255,255,255,.35);
}
.chip.answered:hover, .chip.selection:hover, .chip.attachment:hover { background: #333338; }
.chip .chip-mark, .chip .chip-x { flex: 0 0 auto; }
.chip.attachment .chip-mark { width: 13px; height: 13px; }
.chip.attachment .chip-mark svg { display: block; width: 100%; height: 100%; }
.chip .chip-x {
  appearance: none; -webkit-appearance: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 16px;
  border: none; background: none; cursor: pointer; padding: 0;
  margin-left: -1px;
  font-family: var(--ui-font);
  font-size: 10px; line-height: 1; color: inherit; opacity: .65;
}
.chip .chip-x:hover { opacity: 1; }

/* Staged (answered, not yet sent) pins turn violet-filled with a paused ✓. */
.comment-wrap.announced .comment-dot { background: var(--ui-comment); opacity: .85; }

/* Floating "ask about this" affordance near a text selection. */
/* The selection bar: one pill, two actions. */
.ask-bar {
  display: none;
  position: absolute; z-index: 70;
  align-items: stretch;
  border-radius: 999px;
  background: var(--ui-ink);
  box-shadow: var(--ui-shadow-float);
  overflow: hidden;
}
.ask-bar.shown { display: flex; }
.ask-action {
  border: 0; background: transparent; color: #fff;
  padding: 6px 12px;
  font: inherit; font-size: 12.5px; font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.ask-action:hover { background: rgba(255,255,255,.14); }
.ask-divide { width: 1px; background: rgba(255,255,255,.22); margin: 5px 0; }
/* While messages are live they OWN the row: the input steps aside (the
   prompt is already sent) and the pill grows just enough for the two
   receding older lines above the row slot. */
.composer #prompt::placeholder { color: var(--ui-placeholder-text); opacity: 1; }
/* During a run the prompt stays put, showing what the user typed — frozen,
   not hidden: readonly and slightly dimmed, so it reads as "sent". */
/* Frozen means frozen, and must look it: a field that takes a caret and
   shows an inviting placeholder reads as open even while readOnly. */
.composer #prompt[readonly] { color: var(--ui-glass-text); cursor: not-allowed; }
.composer #prompt[readonly]::placeholder { opacity: .5; font-style: italic; }
.composer #prompt {
  flex: 1; border: none; outline: none;
  font-family: inherit;
  min-width: 0; font-size: 15px; padding: 10px 6px; background: none; color: var(--ui-ink);
}
.attach-input { display: none; }
.composer button {
  border: none; background: var(--ui-ink); color: #fff;
  font-family: inherit;
  font-weight: 600; font-size: 18px; line-height: 1;
  border-radius: 50%; width: 42px; height: 42px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: opacity .15s, transform .15s;
}
.composer button:hover { opacity: .85; transform: scale(1.05); }
.composer .attach-button {
  width: 30px; height: 30px;
  margin-left: 2px;
  background: transparent; color: var(--ui-gray);
  border: none;
  box-shadow: none;
}
.composer .attach-button:hover {
  opacity: 1; color: var(--ui-ink);
  background: transparent;
  transform: none;
}
.composer .attach-button:disabled { opacity: .35; cursor: default; }
/* Stop square while a run owns the row — an active state, never dimmed.
   The glyph needs a smaller size to sit optically like the arrow. */
.composer button.stop { font-size: 11px; }

/* ---------------- Account (sidecar footer) ---------------- */

.sidecar-account {
  margin: auto -14px -24px; /* cancel .sidecar's padding so the rule spans it */
  padding: 12px 14px calc(24px + env(safe-area-inset-bottom));
  border-top: 1px solid rgba(0, 0, 0, .08);
  font-size: 12.5px;
}
.sidecar-account:empty { display: none; }

.account {
  display: flex;
  align-items: center;
  gap: 8px;
}

.account-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  flex: none;
  object-fit: cover;
  background: rgba(0, 0, 0, .08);
}

.account-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
}

.account-signout,
.account-signin {
  font: inherit;
  border: 1px solid rgba(0, 0, 0, .14);
  background: rgba(255, 255, 255, .7);
  border-radius: 8px;
  padding: 5px 9px;
  cursor: pointer;
  color: inherit;
}

.account-signin {
  width: 100%;
  font-weight: 550;
}

.account-signout:hover,
.account-signin:hover { background: #fff; }

.account-dev {
  opacity: .5;
  font-style: italic;
}

/* ---------------- Read-only (someone else holds the lock) ---------------- */
/* The composer stays visible but inert: a watcher should still be able to
   read the document and see the agents working. */

body.read-only .input-row { opacity: .55; }
body.read-only #prompt { cursor: not-allowed; }
body.read-only .comment-reply,
body.read-only .design-button-visible { display: none; }

/* ---------- Share panel ( — owner only) ---------- */
/* Same recipe as .design-button/.design-popover (glass button + fixed popover),
   sitting one slot to its left. Visibility is a class, not [[hidden]]: this
   file already sets .design-button's display explicitly, so a bare [hidden]
   attribute would lose to that rule — see .shown below. */
.share-button {
  display: none;
  align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0;
  border: 1px solid var(--ui-glass-edge); border-radius: 50%;
  background: var(--ui-glass); color: var(--ui-ink);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  cursor: pointer; box-shadow: var(--ui-glass-shadow);
  transition: opacity .15s, transform .15s;
}
.share-button.shown { display: flex; }
.share-button:hover { opacity: .85; transform: scale(1.05); }
.share-button svg { display: block; }

.share-popover {
  display: none;
  position: fixed; top: 64px; right: 72px; z-index: 60;
  width: min(340px, 90vw);
  padding: 16px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  border: 1px solid var(--ui-glass-edge);
  outline: 1px solid var(--ui-glass-edge-dark); outline-offset: -1px;
  border-radius: 16px;
  font-size: 13.5px;
  box-shadow: var(--ui-glass-shadow);
  max-height: min(560px, 78vh);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.share-popover.open { display: block; }
.share-header { margin: 2px 0 12px; }
.share-title { margin: 0; font-weight: 700; font-size: 13px; letter-spacing: .01em; color: var(--ui-ink); }

.share-section + .share-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(0,0,0,.07); }
.share-subtitle { margin: 0 0 10px; font-size: 12.5px; font-weight: 700; color: var(--ui-ink); }
.share-description { margin: 0 0 10px; font-size: 12px; line-height: 1.45; color: var(--ui-gray); }

.share-list { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
/* No links yet: the list must not spend its margin on nothing, or the two
   sections open with visibly different rhythm. */
.share-list:empty { display: none; }
.share-empty { padding: 2px 0 10px; font-size: 12px; color: var(--ui-gray); }

.share-row {
  padding: 9px 10px;
  border: 1px solid rgba(0,0,0,.08);
  border-radius: 12px;
  background: rgba(255,255,255,.5);
}
.share-link-row { display: flex; flex-direction: column; gap: 8px; }
.share-row-main { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; min-width: 0; }
.share-link-url {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 12px; font-weight: 600; color: var(--ui-ink);
}
.share-row-date { flex: 0 0 auto; font-size: 11px; color: var(--ui-gray); }
.share-row-actions { display: flex; gap: 8px; }
.share-row-actions button {
  border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.75); color: var(--ui-ink);
  border-radius: 999px; padding: 5px 12px;
  font-size: 11.5px; font-weight: 600; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.share-row-actions button:hover { background: #fff; }
.share-revoke:hover { color: #a33; border-color: rgba(170,51,51,.35); }
.share-row-actions button:disabled { opacity: .6; cursor: default; }

.share-collaborator-row { display: flex; align-items: center; gap: 10px; }
.share-collaborator-avatar {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; flex: 0 0 auto;
  background: var(--ui-ink); color: #fff;
  font-size: 11px; font-weight: 700; overflow: hidden;
}
.share-collaborator-avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.share-collaborator-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 12.5px; font-weight: 600; color: var(--ui-ink);
}
.share-badge-pending {
  flex: 0 0 auto;
  font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ui-gray); background: rgba(0,0,0,.06);
  border-radius: 999px; padding: 3px 7px;
}
.share-remove {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  border: none; background: none; color: var(--ui-gray);
  font-size: 12px; line-height: 1; cursor: pointer; border-radius: 50%;
  transition: background .15s, color .15s;
}
.share-remove:hover { background: rgba(170,51,51,.1); color: #a33; }
.share-remove:disabled { opacity: .5; cursor: default; }

.share-form { display: flex; gap: 8px; }
/* The text fields, not every input in the form: radios also live here, and
   flex:1 with pill padding stretched them into ovals the width of the panel. */
.share-form input:not([type="radio"]) {
  flex: 1; min-width: 0; border: 1px solid var(--ui-line); border-radius: 999px;
  padding: 8px 14px; font-size: 13px; background: var(--ui-background); outline: none;
  transition: border-color .15s, background .15s;
}
.share-form input:not([type="radio"]):focus { border-color: var(--ui-comment); background: #fff; }
.share-form button {
  border: none; background: var(--ui-ink); color: #fff;
  border-radius: 999px; padding: 8px 16px; font-size: 13px;
  font-weight: 600; cursor: pointer; white-space: nowrap;
  transition: opacity .15s;
}
.share-form button:hover { opacity: .85; }
.share-form button:disabled { opacity: .6; cursor: default; }

/* Invite link: shown under the collaborator list, because inviting grants
   access but sends nothing — the owner has to pass the link on. */
.share-invite-link { margin-top: 10px; }
.share-invite-link[hidden] { display: none; }
/* The row must be a flex container for the url's ellipsis to have anything
   to clip against — an inline span with nowrap just runs out of the box. */
.share-invite-link .share-row {
  margin-top: 4px;
  display: flex; align-items: center; gap: 8px; min-width: 0;
}
.share-invite-link .share-link-url { flex: 1 1 auto; min-width: 0; }
.share-invite-link .share-copy {
  flex: 0 0 auto;
  font: inherit; font-size: 12px;
  border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.75);
  color: var(--ui-ink); border-radius: 999px; padding: 5px 12px; cursor: pointer;
}
.share-invite-link .share-copy:hover { background: #fff; }

/* ---------------- Invite landing ---------------- */
/* Shown when a document link is opened without access — either signed out
   (sign in to continue) or signed in as the wrong account. It covers the
   app rather than sitting inside it: there is no document to show yet. */

.invite-screen {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center; padding: 24px;
  background: var(--ui-background);
}
.invite-screen[hidden] { display: none; }
body.invite-open .composer,
body.invite-open .intro,
body.invite-open .sidecar-button,
body.invite-open .design-button,
body.invite-open .share-button { display: none; }

/* ---------- The app's own confirm and prompt (askConfirm / askText) ---------- */
dialog.ask {
  width: min(400px, calc(100vw - 32px));
  margin: auto; padding: 22px 22px 18px;
  border: 0; border-radius: 18px;
  background: var(--ui-surface); color: var(--ui-ink);
  box-shadow: var(--ui-shadow-float);
  font-family: var(--ui-font);
}
dialog.ask::backdrop { background: rgba(24,24,27,.28); }
.ask-form { display: flex; flex-direction: column; gap: 10px; }
.ask-title { margin: 0; font: 600 15px/1.35 var(--ui-font); color: var(--ui-ink); }
.ask-message { margin: 0; font: 400 13.5px/1.5 var(--ui-font); color: var(--ui-glass-text); }
.ask-message[hidden] { display: none; }
.ask-field { display: flex; flex-direction: column; gap: 6px; margin-top: 2px; }
.ask-field[hidden] { display: none; }
.ask-label { font: 500 12px/1.3 var(--ui-font); color: var(--ui-gray); }
.ask-input {
  width: 100%; height: 38px; padding: 0 12px;
  border: 1px solid var(--ui-line); border-radius: 10px;
  background: var(--ui-surface); color: var(--ui-ink);
  font: 400 14px var(--ui-font);
}
.ask-input:focus { outline: none; border-color: var(--ui-ink); }
.ask-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.ask-cancel, .ask-ok {
  height: 34px; padding: 0 16px; border-radius: 999px;
  font: 500 13.5px var(--ui-font); cursor: pointer;
}
.ask-cancel { border: 1px solid var(--ui-line); background: var(--ui-surface); color: var(--ui-ink); }
.ask-cancel:hover { border-color: var(--ui-ink); }
.ask-ok { border: 0; background: var(--ui-ink); color: #fff; }
.ask-ok:hover { opacity: .88; }
.ask-ok.danger { background: #b42318; }

.invite-card {
  width: min(420px, 100%);
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  text-align: center;
  padding: 34px 30px;
  border-radius: 20px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  box-shadow: var(--ui-glass-shadow);
}
.invite-title {
  margin: 0;
  font: 500 21px/1.3 'Iowan Old Style', 'Palatino', Georgia, serif;
  letter-spacing: -.01em;
}
.invite-text { margin: 0; font-size: 13.5px; color: var(--ui-glass-text); max-width: 34ch; }
.invite-text.invite-error { color: #b3261e; }
.invite-button {
  margin-top: 4px;
  font: inherit; font-weight: 550; font-size: 14px;
  padding: 10px 20px; border: 0; border-radius: 999px;
  background: var(--ui-ink); color: #fff; cursor: pointer;
}
.invite-button:disabled { opacity: .55; cursor: default; }

/* ---------------- Collaborators: overlapping avatars ---------------- */
/* One reserved line whether or not anyone is invited, so inviting somebody
   does not push the rest of the panel down. */

.collaborators-row {
  display: flex; align-items: center;
  min-height: 34px;
  margin-bottom: 4px;
  padding-left: 3px; /* room for the ring on the first avatar */
}

.collaborator {
  flex: 0 0 auto;
  width: 30px; height: 30px; margin-left: -9px;
  padding: 0; border: 2px solid var(--ui-surface); border-radius: 50%;
  background: var(--ui-ink); color: #fff;
  font: 600 12px/1 var(--ui-font);
  display: grid; place-items: center; overflow: hidden; cursor: pointer;
  position: relative;
  transition: transform .16s ease, opacity .16s ease;
}
.collaborator:first-child { margin-left: 0; }
.collaborator img { width: 100%; height: 100%; object-fit: cover; }

/* Not signed in yet: present, but visibly not here. */
.collaborator.pending { opacity: .4; }

.collaborator:hover,
.collaborator:focus-visible,
.collaborator.selected {
  transform: translateY(-2px);
  opacity: 1;
  z-index: 20 !important; /* lift clear of the avatars stacked over it */
}
.collaborator.selected { box-shadow: 0 0 0 2px var(--ui-accent); }

.collaborator-detail {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px; padding: 7px 10px;
  border: 1px solid rgba(0,0,0,.08); border-radius: 12px;
  background: rgba(255,255,255,.5);
  font-size: 12.5px;
}
.collaborator-detail[hidden] { display: none; }
.collaborator-detail-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 600; color: var(--ui-ink);
}
.collaborator-remove {
  flex: 0 0 auto;
  font: inherit; font-size: 12px;
  border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.75);
  color: var(--ui-ink); border-radius: 999px; padding: 4px 11px; cursor: pointer;
}
.collaborator-remove:hover { background: #fff; }

/* ---------------- Team row (top-centre: agents + people) ---------------- */
/* Sits on the same line as the chrome buttons. Agents first, then people,
   then the owner's way to add more. Absolutely centred rather than laid out
   between the buttons, so it does not drift as either side changes width. */

.team {
  position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 60;
  display: flex; align-items: center;
  height: 36px; padding: 0 8px;
  border: 1px solid var(--ui-glass-edge); border-radius: 999px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  box-shadow: var(--ui-glass-shadow);
  max-width: min(46vw, 420px); overflow: hidden;
}
.team[hidden] { display: none; }

.team-member {
  flex: 0 0 auto;
  width: 26px; height: 26px; margin-left: -7px;
  border: 2px solid var(--ui-surface); border-radius: 50%;
  background: var(--ui-ink); color: #fff;
  font: 600 11px/1 var(--ui-font);
  display: grid; place-items: center; overflow: hidden;
  position: relative;
  transition: transform .16s ease;
}
.team-member:first-child { margin-left: 0; }
.team-member img { width: 100%; height: 100%; object-fit: cover; }
.team-member:hover { transform: translateY(-2px); z-index: 5; }

/* One continuous row — agents and people overlap identically, no grouping. */

/* Whoever currently holds the document. Ringed AND lifted above the
   neighbours it overlaps, so "who do I wait for" is answerable at a glance
   rather than by hunting along the row. */
.team-member.holding {
  box-shadow: 0 0 0 2px var(--ui-accent);
  z-index: 10;
  transform: scale(1.08);
}
.team-member.pending { opacity: .4; }

/* Same footprint and the same 2px ring as an avatar, so it sits on the row
   rather than beside it. The glyph is drawn rather than typed: a "+" character
   carries its own font metrics and never lands dead centre. */
.team-plus {
  flex: 0 0 auto;
  width: 26px; height: 26px; margin-left: 8px; padding: 0;
  border: 2px dashed rgba(0,0,0,.22); border-radius: 50%;
  background: transparent; color: var(--ui-glass-text);
  cursor: pointer;
  display: grid; place-items: center;
  transition: color .15s, border-color .15s, background .15s;
}
.team-plus svg { display: block; }
.team-plus:hover {
  color: var(--ui-ink); border-color: rgba(0,0,0,.5);
  background: rgba(255,255,255,.6);
}

/* Narrow windows: the composer and the two corners matter more than this. */
@media (max-width: 720px) { .team { display: none; } }

/* ---------------- Right-hand button row ---------------- */
/* Order, outermost to the corner: feedback, share, design.
   Design sits in the corner because it is used most and is always there —
   the corner is the easiest target on screen. Share is next, a document
   action used now and then. Feedback is outermost: rarest, and the only one
   about the app rather than the document.
   A row rather than three fixed positions, because share disappears when
   there is no document (or you are not its owner) and used to leave a hole
   the width of a button behind it. */
.buttons-right {
  position: fixed; top: 20px; right: 20px; z-index: 60;
  display: flex; align-items: center; gap: 10px;
}
.buttons-right > button { flex: 0 0 auto; }

/* ---------------- Utisci (feedback) ---------------- */

.feedback-button {
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0;
  border: 1px solid var(--ui-glass-edge); border-radius: 50%;
  background: var(--ui-glass); color: var(--ui-ink);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  cursor: pointer; box-shadow: var(--ui-glass-shadow);
  transition: opacity .15s, transform .15s;
}
.feedback-button:hover { opacity: .85; transform: scale(1.05); }
.feedback-button svg { display: block; }

.feedback-popover {
  position: fixed; top: 64px; right: 112px; z-index: 61;
  width: min(320px, calc(100vw - 32px));
  padding: 16px 16px 14px;
  border-radius: 18px;
  background: var(--ui-glass);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  box-shadow: var(--ui-glass-shadow);
  opacity: 0; visibility: hidden; transform: translateY(-6px) scale(.98);
  transform-origin: top right;
  transition: opacity .18s, transform .18s, visibility .18s;
}
.feedback-popover.open { opacity: 1; visibility: visible; transform: none; }

.feedback-title { margin: 0 0 4px; font-size: 14px; font-weight: 600; }
.feedback-description { margin: 0 0 12px; font-size: 12.5px; color: var(--ui-glass-text); }

.feedback-kinds { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.feedback-kind {
  font: inherit; font-size: 12px;
  padding: 5px 10px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.6); color: var(--ui-glass-text);
  transition: background .15s, color .15s, border-color .15s;
}
.feedback-kind:hover { background: #fff; }
.feedback-kind.selected {
  background: var(--ui-ink); border-color: var(--ui-ink); color: #fff; font-weight: 550;
}

#feedback-text {
  width: 100%; resize: vertical; min-height: 76px;
  font: inherit; font-size: 13px; line-height: 1.5;
  padding: 9px 11px; border-radius: 12px;
  border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.7); color: var(--ui-ink);
}
#feedback-text:focus { outline: none; border-color: rgba(0,0,0,.32); background: #fff; }

.feedback-footer { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.feedback-message { flex: 1 1 auto; min-width: 0; font-size: 12px; color: var(--ui-glass-text); }
#feedback-send {
  flex: 0 0 auto;
  font: inherit; font-size: 13px; font-weight: 550;
  padding: 7px 16px; border: 0; border-radius: 999px;
  background: var(--ui-ink); color: #fff; cursor: pointer;
}
#feedback-send:disabled { opacity: .55; cursor: default; }

/* Narrow windows: the team row already hides here; keep the corners usable. */
@media (max-width: 720px) {
  .feedback-popover { right: 16px; left: 16px; width: auto; }
}

/* Attachment chips carry state: still working, or failed to read. A failed
   chip must never look like a ready one — the whole point of the feature is
   that a document cannot quietly ignore what was attached. */
.chip.chip-working { opacity: .6; }
.chip.chip-error {
  border-color: color-mix(in srgb, #b3261e 45%, transparent);
  color: #b3261e;
}
.chip.chip-error .chip-mark { opacity: .8; }

/* @mention picker — appears while typing a comment. Deliberately plain: it is
   a list of names, and picking one inserts the exact roster spelling so the
   match afterwards is never a near miss. */
.mention-picker {
  position: fixed; z-index: 60;
  min-width: 160px; padding: 4px;
  background: var(--ui-surface);
  border: 1px solid var(--ui-line); border-radius: 10px;
  box-shadow: var(--ui-shadow-float);
}
.mention-item {
  display: block; width: 100%;
  padding: 6px 10px; border: 0; border-radius: 7px;
  background: none; color: var(--ui-ink);
  font: 500 13px var(--ui-font); text-align: left; cursor: pointer;
}
/* The way out of an empty list: a different kind of action from naming
   someone, so it reads as one. */
.mention-invite {
  border-top: 1px solid var(--ui-line);
  color: var(--ui-gray);
  font-style: italic;
}
.mention-picker .mention-invite:only-child { border-top: 0; }
.mention-item:hover { background: rgba(0,0,0,.05); }

/* An @mention that resolved to someone on the document. Coloured so it reads
   as a live reference rather than punctuation — this name will be emailed
   when the batch is sent. Only roster matches are marked (see escWithMentions),
   so the colour is a promise the system actually keeps. */
.mention {
  color: var(--ui-accent);
  font-weight: 600;
  border-radius: 4px;
  padding: 0 1px;
  background: color-mix(in srgb, var(--ui-accent) 10%, transparent);
}

/* ---------- Roles: what a commenter sees ---------- */
/* Invited to read and comment, so the composer is not hidden chrome — it is
   a thing they cannot use. The comment lane, the pins and the reply boxes
   all stay: those ARE their tools. */
body.role-commenter .composer { display: none; }
body.role-commenter .design-button { display: none; }

/* ---------- Share panel: the two choices ---------- */
/* Radios, not switches: both options stay readable without touching
   anything. These are decisions with consequences — "comments only" is a
   different product for that person, "public" means no password at all —
   and a switch shows one state while hiding the other behind an
   interaction, leaving the alternative to be inferred from a label that
   moves. */
.share-form-stacked { display: block; }
.share-form-stacked .share-row-inputs {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 10px;
}
.share-form-stacked .share-row-inputs input { flex: 1 1 auto; min-width: 0; }
/* Public: the password has no part to play, but it keeps its place — a form
   that reshuffles under a choice reads as broken. */
.share-form-stacked .share-row-inputs input:disabled { opacity: 0.4; }

.share-choice {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 2px 0 0;
}
.share-option {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ui-ink);
  cursor: pointer;
}
/* The label text sits on the radio's baseline rather than the row's, so a
   two-word option and a four-word one line up with their dots. */
.share-option-text { line-height: 1.3; }
.share-option input[type="radio"] {
  appearance: none;
  width: 15px; height: 15px;
  margin: 0; flex: none;
  border: 1.5px solid #c9c9cf;
  border-radius: 50%;
  cursor: pointer;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.share-option input[type="radio"]:checked {
  border-color: var(--ui-ink);
  /* A filled circle with a ring of background punched out of it — drawn with
     a radial gradient rather than an inset shadow, which at this size left a
     thick donut instead of a dot. */
  background-image: radial-gradient(circle, var(--ui-ink) 0 3.5px, transparent 3.6px);
}
.share-option input[type="radio"]:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }
.share-option input[type="radio"]:disabled { opacity: .5; cursor: default; }
.share-option:hover input[type="radio"]:not(:checked) { border-color: #a1a1aa; }
/* In the collaborator detail the two sit side by side under the name. */
.share-choice-inline { flex-direction: row; gap: 14px; margin: 8px 0 4px; }

/* Which kind of link a row is. Two links that behave differently must not
   look identical in the list. */
/* The empty collaborators row must not hold a tall blank band open. */
.collaborators-row:empty { display: none; }

.share-link-kind {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ui-gray);
}
.share-link-kind.is-public { color: var(--ui-ink); }
/* A commenter's avatar reads as a quieter membership than an editor's. */
.collaborator.commenter { box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 3px var(--ui-line); }

/* The sign-in button, when a Connect click has just pointed at it. Someone
   clicked a row in the side panel and the answer is over here, at the far
   corner of the screen — a tooltip alone can be missed on a wide monitor. */
.account-signin.nudge {
  animation: signin-nudge 1.4s ease-in-out 2;
  box-shadow: 0 0 0 3px rgba(58, 90, 72, .18);
}
@keyframes signin-nudge {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .account-signin.nudge { animation: none; }
}

/* ---------- Projects and people (docs/PROJECTS.md §7, §8) ---------- */

/* A group spans the whole library grid and lays its own cards out inside,
   so the columns stay the same width from one project to the next — a
   per-group `auto-fill` would size a two-report client differently from a
   nine-report one, and the page would read as two different galleries. */
.spec-group { grid-column: 1 / -1; }
.spec-group + .spec-group { margin-top: 26px; }
.spec-group[hidden] { display: none; }
/* .spec-card is display:flex, and an author rule beats the UA's
   [hidden]{display:none} no matter how specific it is — so the composer's
   live filter set `hidden` on cards that stayed on screen. */
.spec-card[hidden] { display: none; }

.spec-group-head {
  /* The title and its brief are two lines; the faces and the buttons centre
     on the TITLE line, not on the pair, so the row reads as one line with a
     caption under it. Baseline alignment put them a few pixels high. */
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 14px;
  margin: 0 0 12px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--ui-line);
}
.spec-group-identity { flex: 1 1 220px; min-width: 0; }
.spec-group-title {
  margin: 0;
  font: 700 16px/1.25 var(--ui-font); letter-spacing: 0; color: var(--ui-ink);
}
.spec-group-brief {
  margin: 3px 0 0;
  font: 400 12.5px/1.4 var(--ui-font); color: var(--ui-gray);
}
.spec-group-brief[hidden] { display: none; }
.spec-group-meta { display: flex; align-items: center; gap: 10px; height: 20px; }
/* Faces overlap the way the collaborators row does, so a five-person client
   costs the same width as a two-person one. */
.spec-group-faces { display: flex; align-items: center; }
.spec-group-face {
  width: 22px; height: 22px;
  margin-left: -6px;
  border: 1.5px solid var(--ui-surface); border-radius: 50%;
  object-fit: cover; background: var(--ui-line);
}
.spec-group-face:first-child { margin-left: 0; }
.spec-group-more {
  margin-left: 5px;
  font: 600 11px var(--ui-font); color: var(--ui-gray);
}
/* The anchor for the header's own "…" panel. */
.spec-group-actions { position: relative; display: flex; align-items: center; gap: 6px; height: 20px; }
/* On a card the panel hangs under a floating button in the corner; here it
   hangs under the last thing in the action row. */
.spec-group-actions .spec-menu { top: 28px; right: 0; }
/* And the button is always visible: a header has no hover state of its own
   to reveal it, and the two settings behind it are the only way to rename or
   delete a project at all. */
.spec-group-actions .spec-card-menu { position: static; opacity: 1; }
.spec-group-button {
  border: 1px solid var(--ui-line); border-radius: 999px;
  background: var(--ui-surface); color: var(--ui-ink);
  padding: 4px 11px;
  font: 600 12px var(--ui-font); cursor: pointer;
}
.spec-group-button:hover { border-color: var(--ui-ink); }
/* One row that scrolls sideways (see wireGroupRow in app.js); the grid is
   what "All N" opens it into. */
.spec-group-row { position: relative; }
.spec-group-cards {
  display: flex; gap: 14px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  padding: 2px 2px 8px;
}
.spec-group-cards > .spec-card { flex: 0 0 180px; scroll-snap-align: start; }
.spec-group.expanded .spec-group-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 20px 14px;
  overflow: visible;
}
.spec-group.expanded .spec-group-cards > .spec-card { flex: initial; }
.spec-group-arrow {
  position: absolute; top: 34%; z-index: 2;
  width: 28px; height: 28px; padding: 0;
  border: 1px solid var(--ui-line); border-radius: 50%;
  background: var(--ui-surface); color: var(--ui-ink);
  font: 400 18px/1 var(--ui-font); cursor: pointer;
  box-shadow: var(--ui-shadow-soft);
  opacity: 0; pointer-events: none; transition: opacity .15s;
}
.spec-group-arrow.prev { left: -14px; }
.spec-group-arrow.next { right: -14px; }
/* Only when there is somewhere to go, and only for a pointer that hovers;
   a finger scrolls the row itself. */
@media (hover: hover) {
  .spec-group-row:hover .spec-group-arrow.on { opacity: 1; pointer-events: auto; }
}
.spec-group.expanded .spec-group-arrow { display: none; }
.spec-group-all { min-width: 0; }

/* The card's own menu: filing is a rare, deliberate act, so it stays out of
   the way until the card is under the pointer — but never for the keyboard,
   which has no hover and would otherwise tab to an invisible control. */
.spec-card-menu {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  border: 1px solid var(--ui-glass-edge-dark); border-radius: 50%;
  background: var(--ui-glass); color: var(--ui-ink);
  -webkit-backdrop-filter: var(--ui-glass-filter); backdrop-filter: var(--ui-glass-filter);
  font: 700 13px/1 var(--ui-font); cursor: pointer;
  opacity: 0; transition: opacity .12s;
}
.spec-card:hover .spec-card-menu,
.spec-card:focus-within .spec-card-menu,
.spec-card-menu:focus-visible { opacity: 1; }
.spec-menu {
  position: absolute; top: 32px; right: 6px; z-index: 3;
  display: flex; flex-direction: column; align-items: stretch;
  min-width: 170px; max-width: 240px; padding: 6px;
  border: 1px solid var(--ui-glass-edge-dark); border-radius: 10px;
  background: var(--ui-surface);
  box-shadow: var(--ui-shadow-float);
}
.spec-menu-label {
  padding: 4px 8px 6px;
  font: 700 10.5px var(--ui-font); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ui-gray);
}
.spec-menu-item {
  padding: 6px 8px;
  border: 0; border-radius: 6px;
  background: none; color: var(--ui-ink);
  text-align: left;
  font: 500 13px var(--ui-font); cursor: pointer;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.spec-menu-item:hover, .spec-menu-item:focus-visible { background: rgba(0,0,0,.05); }
.spec-menu-note {
  padding: 2px 8px 6px;
  font: 400 12px/1.4 var(--ui-font); color: var(--ui-gray);
}

/* The chip for a report that has been told where to land. Same shape as the
   staged chips beside it — it is staged in exactly the same sense: nothing
   has happened yet, and the next send is what spends it. */
.chip.project {
  background: var(--ui-ink); color: #fff;
}
.chip.project:hover { background: #333338; }

/* ---------- The People gallery ---------- */

/* A person's card carries a form and a project picker; the 180px column the
   report thumbnails live in cannot hold either. */
.spec-gallery-grid.people-grid {
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  align-items: start;
}
.people-card {
  display: flex; flex-direction: column; align-items: stretch;
  padding: 14px;
  border: 1px solid var(--ui-line); border-radius: 10px;
  background: var(--ui-surface); color: var(--ui-ink);
  box-shadow: var(--ui-shadow-soft);
}
/* Wraps: "Invited — hasn't signed in yet" cannot share a line with a long
   address, and squeezing them together ellipsised the address — the one
   thing that identifies this person until they sign in. */
.people-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.people-face {
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: 50%; object-fit: cover; background: var(--ui-line);
}
.people-identity { display: flex; flex-direction: column; min-width: 0; }
.people-name {
  margin: 0;
  font: 700 14.5px/1.25 var(--ui-font); letter-spacing: 0; color: var(--ui-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.people-email {
  font: 400 12px/1.35 var(--ui-font); color: var(--ui-gray);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.people-about {
  margin: 10px 0 0;
  font: 400 12.5px/1.45 var(--ui-font); color: var(--ui-ink);
}
/* Nothing said about what they know: the agents cannot route a question to
   them, so the gap is stated rather than left as an empty line. */
.people-about-missing { color: var(--ui-gray); font-style: italic; }
/* Reachable or not. Two facts an owner cannot otherwise tell apart: waiting
   on somebody, and a roster entry that has never gone anywhere. */
.people-status {
  margin-left: auto; align-self: center;
  flex: 0 1 auto; white-space: nowrap;
  font: 600 10.5px/1.35 var(--ui-font); color: var(--ui-accent);
  text-align: right;
}
.people-status-waiting { color: var(--ui-gray); font-weight: 500; }
.people-email[hidden] { display: none; }

.people-projects {
  margin: 8px 0 0;
  font: 600 11px var(--ui-font); letter-spacing: .03em; color: var(--ui-gray);
}
.people-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.people-button {
  border: 1px solid var(--ui-line); border-radius: 999px;
  background: var(--ui-surface); color: var(--ui-ink);
  padding: 5px 12px;
  font: 600 12px var(--ui-font); cursor: pointer;
}
.people-button:hover { border-color: var(--ui-ink); }
.people-button:disabled { opacity: .6; cursor: default; }
.people-button-strong {
  border-color: var(--ui-ink);
  background: var(--ui-ink); color: #fff;
}
.people-button-strong:hover { opacity: .86; }
.people-remove:hover { border-color: #b42318; color: #b42318; }

/* The add form and the edit form are the same fields, so they are the same
   shape — the label sits above the field because one of them is a sentence. */
.people-form .people-name { margin-bottom: 2px; }
.people-edit { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--ui-line); }
.people-edit[hidden] { display: none; }
.people-field { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; }
.people-label { font: 500 12px/1.35 var(--ui-font); color: var(--ui-gray); }
.people-input {
  width: 100%;
  border: 1px solid var(--ui-line); border-radius: 8px;
  background: var(--ui-background); color: var(--ui-ink);
  padding: 7px 10px;
  font: 400 13px var(--ui-font);
  outline: none;
}
.people-input:focus { border-color: var(--ui-ink); background: var(--ui-surface); }
.people-input::placeholder { color: var(--ui-placeholder-text); }

/* One row per (project, person) pair, whichever of the two the card is
   about. A checkbox says whether they are on it, the select says what they
   can do there — and changing the select is itself a way on, so the control
   never sits there doing nothing. */
.people-picker { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--ui-line); }
/* In a project header the picker takes the whole width of the group as a
   panel of its own, under the header and above the cards. Squeezed beside
   the buttons it broke the header's line, and capped narrower than the
   group its rule ended short of the header's, which read as a mistake. */
.spec-group-head .people-picker {
  flex: 1 0 100%; margin: 10px 0 4px; padding: 12px 14px 8px;
  border: 1px solid var(--ui-line); border-radius: 12px;
  background: var(--ui-surface);
}
.people-picker[hidden] { display: none; }
.people-picker-title {
  margin-bottom: 8px;
  font: 700 10.5px var(--ui-font); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ui-gray);
}
.people-picker-note { margin: 0 0 8px; font: 400 12px/1.45 var(--ui-font); color: var(--ui-gray); }
/* Two columns, the role select in a column of its own: with the select
   trailing the name, every row's control sat at a different x. */
.people-picker-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 132px; align-items: center; gap: 12px;
  min-height: 32px;
}
.people-check {
  display: flex; align-items: center; gap: 8px;
  min-width: 0;
  font: 500 13px/1.3 var(--ui-font); color: var(--ui-ink); cursor: pointer;
}
.people-check input[type="checkbox"] {
  width: 15px; height: 15px; margin: 0; flex: none;
  accent-color: var(--ui-ink); cursor: pointer;
}
.people-check input[type="checkbox"]:disabled { opacity: .5; cursor: default; }
.people-picker-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.people-picker-role {
  width: 100%; height: 26px;
  border: 1px solid var(--ui-line); border-radius: 999px;
  background: var(--ui-surface); color: var(--ui-ink);
  padding: 0 8px;
  font: 500 11.5px var(--ui-font); cursor: pointer;
}
.people-picker-role:disabled { opacity: .5; cursor: default; }

/* A client's name is long and a report count is short, so the name is what
   ellipsises. Scoped to this list: the Sources notes are allowed to wrap,
   because "2 ad accounts · 1 page · 1 Instagram" has to. */
#sidecar-projects-list .sidecar-note { flex: 0 0 auto; white-space: nowrap; }

/* The about line on a sidecar person row. Not .sidecar-note: that is the
   uppercase status word the Sources rows use, and a sentence about what
   somebody knows in small caps is unreadable. */
.sidecar-about {
  margin-left: auto; padding-left: 8px;
  max-width: 52%;
  font-size: 11px; color: var(--ui-gray);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}

/* ============================================================
   madSense chrome theme — dead code unless <html data-chrome="madsense">
   is present (that wiring is out of scope for this file).
   ------------------------------------------------------------
   Re-skins the APP SHELL only: page background, sidebar/rail,
   composer/prompt field, menus/popovers, buttons, inputs, badges,
   comment pins, status line, and the chrome's own borders, radii,
   shadows and font. The generated document is never touched — no
   selector below matches .doc, .doc-theme, .block, .block-body, or
   any chart/stat selector; the designer's own CSS keeps full
   ownership of that surface, exactly as before.

   Method: re-map the shell's existing --ui-* custom properties
   (declared in the :root block at the top of this file) to
   madSense's @ms-ui/theme tokens. Nearly every shell rule already
   reads a --ui-* variable instead of a literal colour, so this
   alone reskins the composer, sidecar, comment pins/cards, share/
   design/feedback popovers, mention picker, ask dialog, spec +
   people galleries, chips and the status line for free.

   Values are written as var(--their-token, <literal fallback>).
   This file does not import @ms-ui/theme/tokens.css — wiring that
   in is someone else's call — so the literal fallback is what
   actually renders; the var() reference just means this upgrades
   automatically (including to their own light/dark split) if that
   import ever lands, with nothing here needing to change.

   --ui-ink / --ui-comment → --button-primary (warm dark). --ui-ink
   already did double duty as both body-ish text colour and the
   fill for solid buttons/badges/avatars/tooltips; #322c39 is dark
   enough to read fine as text too, so one mapping covers both
   without splitting the variable. --ui-comment is kept in step
   with --ui-ink deliberately: the default/agent comment pin stays
   a solid "primary" badge, contrasting with .comment-wrap.from-person
   pins, which already key off --ui-accent and so become teal below
   — the pin-colour contrast the app relies on survives unchanged.

   --ui-accent → --teal-primary: rings, the "live"/selected/holding
   states, @mentions, the revision highlight, resolve-hover.

   Per-element overrides further down exist only where a variable
   re-map can't reach: the rule hard-codes a colour outright, or it
   reuses --ui-ink/--ui-comment for what is really a focus ring —
   madSense keys focus to teal (--ring: var(--teal-primary)), not
   the warm-dark primary.
   ============================================================ */

html[data-chrome="madsense"] {
  --ui-background: var(--bg-page, #f6f7f9);
  --ui-surface: var(--bg-white, #ffffff);
  --ui-ink: var(--button-primary, #322c39);
  --ui-comment: var(--button-primary, #322c39);
  --ui-gray: var(--text-secondary, #696d7d);
  --ui-line: var(--border-default, #e1e3e8);
  --ui-accent: var(--teal-primary, #0c8578);
  --ui-glass-text: var(--text-secondary, #696d7d);
  --ui-placeholder-text: var(--text-tertiary, #8d91a0);
  --ui-shadow-soft: var(--shadow-card, 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04));
  --ui-shadow-float: var(--shadow-dropdown, 0 4px 16px rgba(0,0,0,.1));
  --ui-glass-shadow: var(--shadow-dropdown, 0 4px 16px rgba(0,0,0,.1));
  --ui-font: var(--font-sans, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
  --ui-paper: var(--bg-white, #ffffff);
  /* Consumed only by the per-element overrides below, so every
     mode-specific value lives in this block and its dark twin —
     the overrides themselves need no light/dark split of their own. */
  --ui-focus-ring: var(--teal-primary, #0c8578);
  --ui-positive: var(--positive, #007a55);
  --ui-negative: var(--negative, #c70036);
  /* --ui-ink does double duty as body-ish TEXT colour and the FILL
     for solid buttons/badges/avatars/tooltips/pins. One warm-dark
     value can stand in for both here only because it happens to
     read fine as text too (see the light-mode note further up) —
     in the dark block below the two roles split for real. --ui-solid
     is what every filled-control override further down reads, so
     it stays correct in both modes without depending on which way
     --ui-ink is pointed. NOT named --ui-fill: that custom property
     already exists in this file as a PERCENTAGE (the design-effort
     slider's fill position, set inline per-element by app.js — see
     .design-popover input[type=range]::-webkit-slider-runnable-track
     below) — reusing the name for a colour here would collide with
     it. */
  --ui-solid: var(--button-primary, #322c39);
  --ui-solid-text: var(--text-inverse, #ffffff);
  /* The analyst's mark (.ms-mark): madSense's own teal, on the surface. */
  --ui-mark: var(--teal-primary, #0c8578);
}

/* Dark twin. Keyed to data-theme, NOT prefers-color-scheme: the madSense
   dashboard resolves light/dark/system itself (@ms-ui/theme) and hands
   the frame the RESOLVED theme with the session (docs/MADSENSE.md §5);
   public/auth.js mirrors it into <html data-theme>. A media query here
   would paint a dark frame inside a light dashboard on a dark Mac. */
html[data-chrome="madsense"][data-theme="dark"] {
  --ui-background: var(--bg-page, #0e0e11);
  --ui-surface: var(--bg-white, #16161b);
  /* TEXT role: madSense's dark --text-primary, not --button-primary.
     --button-primary (#312a3b) is a dark FILL colour meant to sit
     UNDER light text, not to BE text on a dark page — that mismatch
     was making headlines and sidecar labels near-invisible. See the
     --ui-solid note in the light block above for the fill half of
     this split; --ui-comment stays --button-primary on purpose,
     because every one of its own uses is a fill (the comment pins),
     not text — its one text-like use, .comment .author, is
     overridden separately further down to read --ui-ink instead. */
  --ui-ink: var(--text-primary, #f5f4f8);
  --ui-comment: var(--button-primary, #312a3b);
  --ui-gray: var(--text-secondary, #aaa8ba);
  --ui-line: var(--border-default, #2b2a32);
  --ui-accent: var(--teal-primary, #31d3bf);
  --ui-glass-text: var(--text-secondary, #aaa8ba);
  --ui-placeholder-text: var(--text-tertiary, #838194);
  --ui-shadow-soft: var(--shadow-card, 0 1px 0 rgba(255,255,255,.03), 0 14px 32px rgba(0,0,0,.28));
  --ui-shadow-float: var(--shadow-dropdown, 0 18px 48px rgba(0,0,0,.42));
  --ui-glass-shadow: var(--shadow-dropdown, 0 18px 48px rgba(0,0,0,.42));
  --ui-glass: rgba(22,22,27,.8);
  --ui-glass-edge-dark: rgba(255,255,255,.09);
  --ui-paper: var(--bg-white, #16161b);
  --ui-focus-ring: var(--teal-primary, #31d3bf);
  --ui-positive: var(--positive, #5bd99f);
  --ui-negative: var(--negative, #ff7088);
  --ui-solid: var(--button-primary, #312a3b);
  --ui-solid-text: var(--text-inverse, #ffffff);
  --ui-mark: var(--teal-primary, #31d3bf);
}

/* Text-input and composer focus states hard-code their border/
   outline colour (via --ui-ink or --ui-comment, already remapped
   above to the warm-dark primary) instead of reading a ring
   variable, so the re-map alone would leave every focus state
   warm-dark. madSense's own components key focus to teal instead. */
html[data-chrome="madsense"] .comment textarea:focus,
html[data-chrome="madsense"] .design-popover input:focus,
html[data-chrome="madsense"] .share-form input:not([type="radio"]):focus,
html[data-chrome="madsense"] .ask-input:focus,
html[data-chrome="madsense"] .people-input:focus,
html[data-chrome="madsense"] #feedback-text:focus {
  border-color: var(--ui-focus-ring);
  background: var(--ui-surface);
}
html[data-chrome="madsense"] .composer form:focus-within {
  outline-color: var(--ui-focus-ring);
}

/* The section tabs above the composer sit on a baseline. On mailbatch.ai the
   tabs merge into the pill's edge; inside madSense the user wants the line
   they stand on to be seen — their teal, the same colour as the pill's ring. */
html[data-chrome="madsense"] .context-strip.has-items {
  /* A hairline, and only as wide as the tabs themselves: the strip otherwise
     spans the pill and the line ran on past the last tab. */
  border-bottom: 1px solid var(--ui-accent);
  padding-bottom: 0;
  right: auto; width: max-content; max-width: calc(100% - 84px);
}

/* The status line's default colour already reads --ui-glass-text
   (remapped above); only these two modifiers hard-code hex. */
html[data-chrome="madsense"] .status-text.success { color: var(--ui-positive); }
html[data-chrome="madsense"] .status-text.error { color: var(--ui-negative); }

/* Comment-pin avatar ring: hard-coded white border (plus initials
   colour) so the dot "floats" off the page — fine in a light-only
   world, but a literal white ring glares on a dark surface. Pinned
   to the shell surface colour instead, the same treatment the
   --ui-surface-based ring on .block-agent-face already gets. */
html[data-chrome="madsense"] .comment-dot,
html[data-chrome="madsense"] .comment-dot .dot-top,
html[data-chrome="madsense"] .comment-dot .dot-peek {
  border-color: var(--ui-surface);
}

/* ------------------------------------------------------------
   Dark-mode correction: --ui-ink / --ui-comment double duty.
   --ui-ink served two roles — body-ish TEXT colour and the FILL
   for solid buttons/badges/avatars/tooltips/pins — and one warm-
   dark value could stand in for both only because both modes used
   to share the same dark tone. The dark block above now points
   --ui-ink at madSense's dark --text-primary (#f5f4f8) so headings,
   labels and body text are legible on a dark page; --ui-comment is
   untouched (still --button-primary) because its own uses are
   fills, not text — see below.

   That correctly breaks every FILL rule that was reading --ui-ink
   for a background/border/gradient/accent-color: those need to
   STAY the warm-dark primary in both modes, same as a filled
   button elsewhere. --ui-solid / --ui-solid-text (added to both
   variable blocks above) hold that fill colour and the text/icon
   colour that sits on it; every filled-control rule below reads
   them instead of --ui-ink. In light mode --ui-solid equals
   --ui-ink's old value exactly, so none of this changes light-mode
   rendering — only dark mode, where a filled control now stays a
   warm-dark fill instead of turning light-text-on-light-fill.

   Classification of every var(--ui-ink) / var(--ui-comment) site
   found by grep, kept here for the record:

   TEXT (no override needed — --ui-ink's corrected value fixes it):
   body, .intro, .block-failed-retry, .building, .block-comment-
   button, .block-agent-name, .comment-avatar-letter, .comment-text,
   .qa-answer, .connect-offer-skip:hover, .sidecar-button, .sidecar-
   item, .sidecar-item.connected .sidecar-note, .sidecar-all:hover,
   .spec-card, .spec-card-title, .connector-card (its `color`; the
   card's own `background:#fff` is a separate hard-code, handled
   below), .connector-plus (its `color`), .design-button, .design-
   title, .design-effort-value, .composer #prompt, .attach-button:
   hover, .share-button, .share-title, .share-subtitle, .share-link-
   url, .share-row-actions button (its `color`), .share-collaborator-
   name, .share-invite-link .share-copy (its `color`), dialog.ask,
   .ask-title, .ask-input (its `color`), .ask-cancel (its `color`),
   .collaborator-detail-name, .collaborator-remove (its `color`),
   .feedback-button, #feedback-text (its `color`), .mention-item,
   .share-option, .share-link-kind.is-public, .spec-group-title,
   .spec-group-button (its `color`), .spec-group-arrow (its `color`),
   .spec-card-menu (its `color`), .spec-menu-item, .people-card (its
   `color`), .people-name, .people-about, .people-button (its
   `color`), .people-check, .people-picker-role (its `color`),
   .team-plus:hover.

   FILL, overridden below to --ui-solid / --ui-solid-text: every
   background / matched border / gradient / accent-color use, listed
   next to its rule further down. Deliberately NOT overridden:
   .block-failed-retry:hover, .ask-cancel:hover, .spec-group-
   button:hover and .people-button:hover only darken a BORDER on an
   otherwise-unfilled outline button on hover — in dark mode
   --ui-ink's corrected value now draws a LIGHT hover border there,
   which reads fine on an outline button, so adding four more
   override rules for a cosmetic hover felt like the wrong trade.
   .comment textarea:focus, .design-popover input:focus, .share-form
   input:focus, .ask-input:focus and .people-input:focus already
   read --ui-focus-ring (teal) from the overrides added earlier and
   needed no reclassification.

   --ui-gray: every use (~40 sites, all `color:`) is text; none is a
   fill, so it needed no split. */

/* .comment .author is the one place --ui-comment is read as TEXT
   (the commenter's name) rather than as the pin fill it is
   everywhere else; repointed at --ui-ink so it goes light in dark
   instead of staying warm-dark-on-dark. */
html[data-chrome="madsense"] .comment .author {
  color: var(--ui-ink);
}

/* Filled controls: solid buttons, the send button, badges, avatars,
   tooltips, chips, staged pills. Stay warm-dark-fill /
   light-on-fill text in both modes. */
html[data-chrome="madsense"] .comment-user-avatar,
html[data-chrome="madsense"] .comment button,
html[data-chrome="madsense"] .connector-button,
html[data-chrome="madsense"] .connector-actions button,
html[data-chrome="madsense"] .design-button::before,
html[data-chrome="madsense"] .design-popover button,
html[data-chrome="madsense"] .context-hint,
html[data-chrome="madsense"] .composer button,
html[data-chrome="madsense"] .share-collaborator-avatar,
html[data-chrome="madsense"] .share-form button,
html[data-chrome="madsense"] .ask-ok,
html[data-chrome="madsense"] .invite-button,
html[data-chrome="madsense"] .collaborator,
html[data-chrome="madsense"] .team-member,
html[data-chrome="madsense"] #feedback-send,
html[data-chrome="madsense"] .chip.project {
  background: var(--ui-solid);
  color: var(--ui-solid-text);
}
html[data-chrome="madsense"] .connect-offer-go {
  background: var(--ui-solid);
  color: var(--ui-solid-text);
  border-color: var(--ui-solid);
}
html[data-chrome="madsense"] .connector-badge,
html[data-chrome="madsense"] .connector-plus {
  background: var(--ui-color, var(--ui-solid));
  color: var(--ui-solid-text);
}
html[data-chrome="madsense"] .chip.answered,
html[data-chrome="madsense"] .chip.selection,
html[data-chrome="madsense"] .chip.attachment {
  background: var(--ui-solid);
  color: var(--ui-solid-text);
  border-color: var(--ui-solid);
}
html[data-chrome="madsense"] .feedback-kind.selected {
  background: var(--ui-solid);
  color: var(--ui-solid-text);
  border-color: var(--ui-solid);
}
html[data-chrome="madsense"] .people-button-strong {
  border-color: var(--ui-solid);
  background: var(--ui-solid);
  color: var(--ui-solid-text);
}

/* Same fill, non-background properties: a progress ring, two slider
   fills (the webkit gradient keeps its own var(--ui-fill, 25%)
   untouched — that is an unrelated PERCENTAGE custom property set
   inline per-element by app.js for the effort thumb's position, not
   a colour — see the naming note on --ui-solid above), a tooltip
   arrow, a selection ring, a checked radio, and a checkbox's native
   accent colour. */
html[data-chrome="madsense"] .design-button::after {
  background: conic-gradient(var(--ui-solid) 0 var(--ui-design-progress, 0%), rgba(24,24,27,.12) 0 100%);
}
html[data-chrome="madsense"] .design-popover input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg, var(--ui-solid) var(--ui-fill, 25%), rgba(0,0,0,.12) var(--ui-fill, 25%));
}
html[data-chrome="madsense"] .design-popover input[type=range]::-moz-range-progress {
  background: var(--ui-solid);
}
html[data-chrome="madsense"] .context-hint::after {
  background: var(--ui-solid);
}
html[data-chrome="madsense"] .spec-card.active-f .spec-thumb {
  border-color: var(--ui-solid);
  box-shadow: inset 0 0 0 1px var(--ui-solid), var(--ui-shadow-soft);
}
html[data-chrome="madsense"] .share-option input[type="radio"]:checked {
  border-color: var(--ui-solid);
  background-image: radial-gradient(circle, var(--ui-solid) 0 3.5px, transparent 3.6px);
}
html[data-chrome="madsense"] .people-check input[type="checkbox"] {
  accent-color: var(--ui-solid);
}

/* ------------------------------------------------------------
   Hard-coded light backgrounds in popovers, menus, the composer's
   context chips, and card surfaces — none of these read --ui-ink
   or --ui-comment at all, so the split above doesn't touch them,
   but a literal #fff / rgba(255,255,255,…) glares on a dark page.
   The popovers/menus THEMSELVES (.share-popover, .design-popover,
   .feedback-popover, .mention-picker, .spec-menu, .sidecar, .team,
   .comment, dialog.ask, …) already read --ui-glass or --ui-surface
   and needed nothing; this is the smaller rows/pills/cards inside
   them that hard-code white instead.

   Mapped to --ui-surface, at each rule's original opacity via
   color-mix so translucent rows stay translucent. In light mode
   --ui-surface is #ffffff, so e.g. color-mix(…,50%,transparent)
   reproduces the original rgba(255,255,255,.5) exactly — dark mode
   is the only place any of this changes what renders. Left alone
   deliberately: the two native range-input thumbs (::-webkit-
   slider-thumb, ::-moz-range-thumb, both background:#fff) are a
   small, theme-neutral knob treatment, not chrome surface; and
   .spec-thumb's own background:#fff is a literal white "page"
   placeholder behind the gallery's iframe preview — it is standing
   in for a document page, not for shell chrome, so it stays white
   in both modes on purpose (.spec-thumb.empty already reads
   --ui-paper, fixed earlier). Also out of scope here: the many
   rgba(0,0,0,.0x) hover/border tints elsewhere in the shell (e.g.
   .sidecar-item:hover, .mention-item:hover) — those under-perform
   rather than glare in dark mode (a black tint on an already-dark
   surface reads as almost nothing, not as a bright mistake), which
   is a real but different problem from the one asked about here. */
html[data-chrome="madsense"] .share-row,
html[data-chrome="madsense"] .collaborator-detail {
  background: color-mix(in srgb, var(--ui-surface) 50%, transparent);
}
html[data-chrome="madsense"] .feedback-kind {
  background: color-mix(in srgb, var(--ui-surface) 60%, transparent);
}
html[data-chrome="madsense"] .connector-add {
  background: color-mix(in srgb, var(--ui-surface) 64%, transparent);
}
html[data-chrome="madsense"] .account-signout,
html[data-chrome="madsense"] .account-signin,
html[data-chrome="madsense"] #feedback-text {
  background: color-mix(in srgb, var(--ui-surface) 70%, transparent);
}
html[data-chrome="madsense"] .share-row-actions button,
html[data-chrome="madsense"] .share-invite-link .share-copy,
html[data-chrome="madsense"] .collaborator-remove {
  background: color-mix(in srgb, var(--ui-surface) 75%, transparent);
}
html[data-chrome="madsense"] .account-signout:hover,
html[data-chrome="madsense"] .account-signin:hover,
html[data-chrome="madsense"] .share-row-actions button:hover,
html[data-chrome="madsense"] .share-invite-link .share-copy:hover,
html[data-chrome="madsense"] .collaborator-remove:hover,
html[data-chrome="madsense"] .feedback-kind:hover,
html[data-chrome="madsense"] .connector-card,
html[data-chrome="madsense"] .connector-card.announced {
  background: var(--ui-surface);
}
html[data-chrome="madsense"] .chip {
  background: var(--ui-surface);
}
html[data-chrome="madsense"] .chip:hover {
  background: color-mix(in srgb, var(--ui-surface) 90%, var(--ui-ink) 10%);
}

/* Sources and Agents never render inside the dashboard, whatever app.js has
   had time to do: the source is the brand their header chose and the roster
   is fixed (docs/MADSENSE.md). auth.js puts data-chrome on <html> before its
   first await, so this holds from the first paint; the attribute
   hideEmbeddedSidecarSections() sets once /api/me has answered is the same
   decision a round trip later. Their "See all" rows sit inside the sections
   and are named here as well, so nothing can bring one back on its own. */
html[data-chrome="madsense"] #sidecar-sources,
html[data-chrome="madsense"] #sidecar-agents,
html[data-chrome="madsense"] .sidecar-all[data-group="sources"],
html[data-chrome="madsense"] .sidecar-all[data-group="agents"] {
  display: none !important;
}

/* The composer's "+": a bare glyph beside the solid send button. On
   mailbatch.ai it is a quiet 30px glyph and stays that way; here the theme
   paints every .composer button solid (the rule above), which made it a
   second solid 42px circle — and two solid circles read as two send buttons.
   So: no circle, no fill, no border, no shadow. The hit area keeps the send
   button's 42px so .input-row centres the two on one line; only the glyph is
   drawn, 20px in the chrome's ink, and hovering dims it rather than lifting
   it, because there is no button to lift. */
html[data-chrome="madsense"] .composer .attach-button {
  width: 42px; height: 42px; margin-left: 0;
  background: transparent; color: var(--ui-ink);
  border: 0; border-radius: 0; box-shadow: none;
}
html[data-chrome="madsense"] .composer .attach-button svg { width: 20px; height: 20px; }
html[data-chrome="madsense"] .composer .attach-button:hover:not(:disabled) {
  background: transparent; color: var(--ui-ink);
  opacity: .7; transform: none;
}
