/* Cedreon chat, loaded after app.css on /console/chat.
 *
 * The reading rules, in one place, because everything below follows from them:
 *
 *   - The ASSISTANT does not live in a card. A screened answer arrives whole,
 *     and prose on the canvas is what a document looks like. No avatar, no name
 *     label, no border: the only thing on that side of the column is the answer.
 *   - YOU live in a bubble, right aligned and compact. The asymmetry is the
 *     whole layout: one glance tells you who said what, with no labels at all.
 *   - GREEN means protection and nothing else. NAVY means action and nothing
 *     else. There is no third accent.
 *   - Every colour comes from a base.html token, so Warm and Navy swap the
 *     whole surface without one rule here being restated per appearance.
 *
 * Sovereign: no url(), no import. */

#chat-app { flex: 1; display: flex; min-height: 0; min-width: 0; width: 100%; }

/* -------------------------------------------------- conversations (rail) --- */
/* New chat and Search read as rows of the list they sit above, not as two
   boxed widgets: same padding and radius as a conversation, no borders. The
   list below is what this rail is for. */
.rail-folder { width: 34px; height: 34px; flex: 0 0 auto; display: grid;
               place-items: center; background: none; color: var(--s-muted);
               border: 0; border-radius: 9px; cursor: pointer; }
.rail-folder:hover { background: var(--s-hover); color: var(--s-ink); }
/* Signed out, the rail's bottom holds the two auth entries. They are LABELS
   for the auth card's radio tabs (the card is always open, so a click just
   flips the tab with no script), hence the flex centring. */
.rail-auth { margin-top: auto; padding: 12px 14px;
             border-top: 1px solid var(--s-hair);
             display: flex; flex-direction: column; gap: 8px; }
.rail-auth-btn { height: 36px; border-radius: 10px; font-size: 12.5px;
                 font-weight: 600; cursor: pointer;
                 font-family: var(--font-sans);
                 display: flex; align-items: center; justify-content: center;
                 background: var(--surface); color: var(--ink);
                 border: 1px solid var(--border-2); }
.rail-auth-btn:hover { border-color: var(--graphite); }
.rail-auth-btn.strong { background: var(--accent); border-color: var(--accent);
                        color: var(--on-accent); }
.rail-auth-btn.strong:hover { background: var(--accent-strong);
                              border-color: var(--accent-strong); }
.folder-hint { padding: 6px 9px; font-size: 11px; color: var(--s-muted); }
.group-label { padding: 14px 10px 5px; }
.group-label .kicker { color: var(--s-muted); opacity: .82; }

.convo-more { visibility: hidden; display: grid; place-items: center;
              width: 22px; height: 22px; flex: 0 0 auto; border: 0;
              border-radius: 6px; background: none; color: var(--s-muted);
              cursor: pointer; font-size: 13px; line-height: 1; }
.convo-item:hover .convo-more, .convo-more:focus-visible { visibility: visible; }
.convo-more:hover { color: var(--s-strong); background: var(--s-active); }
.convo-rename-row { padding: 3px 4px; }
.convo-rename { width: 100%; height: 32px; padding: 0 9px;
                background: var(--s-hover);
                border: 1px solid var(--s-active); border-radius: 8px;
                font: inherit; font-size: 12.5px; color: var(--s-ink);
                box-sizing: border-box; }
.convo-rename:focus { outline: none; border-color: var(--accent); }

/* Dragging a chat onto a MATTER. The folder head, the folder body and both
   halves of a recency group used to be drop targets too -- the recency ones
   taking the chat back OUT of a folder -- and all four are gone with the
   grouping. `.rail-project.drop-target` in app.css is the one that remains.
   `.ungrouped-body` keeps its box: it is still the body of every recency
   group, it just no longer accepts a drop. */
.convo-item.dragging { opacity: .45; }
.ungrouped-body { min-height: 8px; border-radius: 9px; }

.convo-menu { position: fixed; z-index: 80; min-width: 190px;
              background: var(--surface); border: 1px solid var(--border);
              border-radius: 12px; box-shadow: var(--e3); padding: 5px; }
.convo-menu .menu-kicker { padding: 6px 9px 4px; }
.convo-menu button { display: block; width: 100%; text-align: left; border: 0;
                     background: none; border-radius: 8px; padding: 7px 9px;
                     font: inherit; font-size: 12.5px; color: var(--ink);
                     cursor: pointer; }
.convo-menu button:hover { background: var(--hover); }
.convo-menu button.danger { color: var(--bad); }
.convo-menu hr { border: 0; border-top: 1px solid var(--border); margin: 4px 0; }

/* -------------------------------------------------------------- chat pane --- */
#chat-pane { flex: 1; display: flex; flex-direction: column;
             min-height: 0; min-width: 0; background: var(--canvas); }
/* The header carries no rule: the transcript below it fades under the composer
   instead, and a hairline across the top of a reading column is one horizontal
   line too many. */
.chat-top { display: flex; align-items: center; gap: 8px; padding: 12px 18px;
            flex: 0 0 auto; }
#sidebar-toggle { display: grid; place-items: center; width: 32px;
                  height: 32px; flex-shrink: 0; border: 0; border-radius: 8px;
                  background: none; color: var(--graphite); cursor: pointer;
                  transition: background .12s ease, color .12s ease; }
#sidebar-toggle:hover { color: var(--ink); background: var(--hover); }
.chat-title { flex: 1; min-width: 0; display: flex; align-items: center;
              gap: 8px; }
/* The conversation's name, at the weight of a label rather than a page title:
   the answer below it is the page. */
#chat-heading { font-family: var(--font-sans); font-size: 14px; font-weight: 600;
                margin: 0; white-space: nowrap; letter-spacing: 0;
                overflow: hidden; text-overflow: ellipsis; max-width: 48vw; }
/* THE HEADING IS PRESSABLE on a saved conversation (§ item 8): the name is
   edited where it is read, by the rail's own rename handler. A `button`
   rather than a click on the h1, so the keyboard reaches it and assistive
   tech announces an action -- and every UA default the element brings with it
   is stripped here, because a heading that quietly grew a border, a system
   font and a centred baseline is the change nobody asked for. */
/* THE PADDING IS THE HEADING'S, not the button's, and that is not cosmetic.
   The h1 is a flex item sized to fit its content; a padded button inside it
   was then clamped by `max-width: 100%` to the h1's CONTENT box, so the
   padding came out of the words and "Screened chat" rendered as "Screened
   c...". The hover target is the same size either way. */
#chat-heading.is-editable { padding: 2px 6px; margin-left: -6px;
                            border-radius: 7px;
                            transition: background .12s ease; }
#chat-heading.is-editable:hover { background: var(--hover); }
.chat-heading-btn { display: block; max-width: 100%; margin: 0; padding: 0;
                    border: 0; background: none;
                    font: inherit; color: inherit; letter-spacing: inherit;
                    text-align: left; cursor: text;
                    overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; }
/* While the field is open the heading GROWS, and it has to: the h1 is sized to
   fit its content, and an input's content width is the element's own default
   (about twenty characters), so a 40-character name opened into a box it
   scrolled inside. The clipping goes with it -- there is nothing to ellipsise
   in a field somebody is typing in. */
#chat-heading.is-renaming { flex: 1 1 auto; min-width: 0; overflow: visible; }
/* The field it swaps for. Same face, so the name does not change shape when
   the edit starts. */
.chat-rename { width: 100%; max-width: min(360px, 48vw);
               padding: 2px 6px; margin-left: -6px;
               border: 1px solid var(--accent); border-radius: 7px;
               background: var(--surface); color: var(--ink);
               font: inherit; }
.chat-rename:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-tint); }
.whoami { display: none; }
/* A new chat has no title yet, so the heading would be a dead affordance
   above a centred greeting -- and it is a rename BUTTON now, which is a
   stronger reason to keep it out of the way rather than a weaker one.
   `.model-card` used to be listed here too; the header's ⓘ is gone, and the
   `.turn-model` disclosure that reuses the class only exists on an answer,
   which is by definition not an empty chat. */
#chat-app[data-empty] #chat-heading { opacity: 0; pointer-events: none; }

.model-card { position: relative; font-size: 12px; color: var(--faint); }
.model-card summary { cursor: pointer; list-style: none; display: grid;
                      place-items: center; width: 28px; height: 28px;
                      border-radius: 8px; }
.model-card summary:hover { color: var(--graphite); background: var(--hover); }
.model-card summary::-webkit-details-marker { display: none; }
.model-card[open] summary { color: var(--accent); }
.model-card .cardinfo { position: absolute; z-index: 30; top: calc(100% + 6px);
                        left: 0; width: 330px; max-width: 76vw;
                        background: var(--surface); border: 1px solid var(--border);
                        border-radius: 12px; box-shadow: var(--e2);
                        padding: 10px 12px; line-height: 1.55; color: var(--ink); }

.vault-pill { display: inline-flex; align-items: center; gap: 6px;
              padding: 5px 12px; border: 0; border-radius: 999px;
              background: var(--ok-tint); color: var(--ok-ink);
              font-size: 11.5px; font-weight: 600; cursor: pointer;
              white-space: nowrap; font-family: var(--font-sans); }
.vault-pill:hover { background: var(--ok-tint); filter: brightness(.97); }
.vault-pill .dot { width: 6px; height: 6px; border-radius: 50%;
                   background: var(--ok); }
/* Switched off. NEUTRAL rather than `--bad`: a vault the account turned off is
   a choice it made, and an alarm colour would report a fault where there is a
   setting. The green is the whole signal here -- a pill that kept it and only
   changed four letters would go on reading as "protected" at a glance, which
   is how the label came to contradict the switch in the first place. */
.vault-pill.off { background: var(--hover); color: var(--ink-2); }

/* Naming a value from the PANE. The same form as the one on Guardrails, in a
   column the settings page does not have to fit: at ~300px a text field and a
   button side by side leave the field too short to read back what was typed,
   so this one stacks. `.vault-add` itself lives in guardrails.css and is not
   loaded here, which is why the layout is restated rather than overridden. */
.vault-add-compact { display: flex; flex-direction: column; gap: 6px;
    margin: 2px 0 14px; }
.vault-add-compact input[type="text"] { font-size: 12px; padding: 7px 10px;
    background: var(--paper); border: 1px solid var(--border);
    border-radius: 9px; color: var(--ink); width: 100%; }
.vault-add-compact input[type="text"]:focus { outline: none;
    border-color: var(--graphite); }
.vault-add-compact input[type="text"][aria-invalid="true"] {
    border-color: var(--bad); }
.vault-add-compact .btn { align-self: flex-start; white-space: nowrap; }
.vault-add-compact .vault-add-error { margin: 0; font-size: 11.5px;
    color: var(--bad); }

.vault-pill.off:hover { background: var(--hover); filter: brightness(.97); }
.vault-pill.off .dot { background: var(--ink-2); opacity: .55; }

/* The reading column, shared by the transcript and the composer so a reply and
   the box that answered it line up on the same two edges. */
/* `overscroll-behavior: contain` for the same reason the menus above carry it,
   and it matters most here: reaching the top or the bottom of the transcript
   used to hand the leftover scroll to the page, and on a phone that gesture is
   what expands and collapses the browser's own chrome. The shell measures
   itself in dvh now, so every one of those handovers resized the layout under
   the reader's thumb. The transcript keeps its own overscroll. */
#msgs { flex: 1; overflow-y: auto; padding: 8px 28px 10px;
        overscroll-behavior: contain; scrollbar-width: thin; }
#msgs > * { max-width: var(--measure); margin-left: auto; margin-right: auto; }
#msgs::-webkit-scrollbar { width: 10px; }
#msgs::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 8px;
                                 border: 3px solid var(--canvas); }

/* ------------------------------------------------------- first-run home --- */
/* On an empty chat the greeting, composer and example prompts ride together in
   the middle of the pane. The centring is pure flexbox inside the existing
   #chat-pane column: #msgs stops eating the slack (flex:0 1 auto) and the free
   space is split by the two auto margins, so nothing is viewport-positioned and
   the block still narrows correctly when the side pane opens. */
/* The greeting is centred over the composer: on an empty pane there is no
   column of text for it to align to, so left-aligning it hung it off nothing.
   Centred, it reads as the room's one line rather than the first line of a
   transcript that does not exist yet. */
#empty-state { text-align: center; }
/* The brand ring above the first-run greeting: the favicon shape at hero
   size, in the product's brand tone. Canvas side, so --brand (Navy lifts it
   with its palette).

   NO TOP MARGIN. There used to be 9vh here, on the reasoning that the block
   "read too high without it" -- but 9vh is not a nudge, it is 81px on a
   laptop and 160px on a tall display, and it sat on TOP of the auto margin
   that was already centring the group. The result was a quarter of the
   window empty above a ring, growing with the screen. The lift the comment
   wanted is real and is still here; it comes from the composer's own bottom
   padding below, which is in rem and therefore the same nudge at every
   height. */
.empty-mark { display: flex; justify-content: center; margin: 0 0 26px; }
.empty-ring { width: 76px; height: 76px; border: 9px solid var(--brand);
              border-radius: 50%; }
/* One line, no sub-line: the greeting says hello and gets out of the way (the
   composer right below it is the actual invitation). */
.empty-line { font-family: var(--font-display); font-size: 30px;
              font-weight: 400; letter-spacing: -.015em; color: var(--ink);
              margin: 0 0 6px; text-wrap: balance; }

/* THE INSTALL INVITATION, under the greeting (§77.3). Deliberately quiet: the
   composer below is the invitation this screen is for, and a panel that
   competed with it would be an advertisement in the middle of somebody's
   work. So it is one line of text at reading size, a link and a way to make
   it go away, on the canvas rather than on a card of its own.

   Left-aligned inside a centred block: `#empty-state` centres its children,
   and a two-line sentence centred under a centred greeting reads as a second
   headline rather than as a note. The BOX is centred; the words are not. */
.desktop-offer { display: flex; flex-wrap: wrap; align-items: baseline;
                 gap: 6px 14px; max-width: 520px; margin: 26px auto 0;
                 padding: 14px 16px; text-align: left;
                 border: 1px solid var(--border); border-radius: 10px;
                 background: var(--surface); }
.desktop-offer-t { flex: 1 1 260px; font-size: 13px; line-height: 1.55;
                   color: var(--ink-2); }
.desktop-offer-t strong { display: block; font-weight: 600; color: var(--ink); }
.desktop-offer-a { display: flex; align-items: center; gap: 14px; }
.desktop-offer-a a { font-size: 13px; font-weight: 600; color: var(--accent);
                     text-decoration: none; }
.desktop-offer-a a:hover { text-decoration: underline; }
/* A plain button and not a MiniButton: dismissing is the weaker of the two
   actions here and must not be dressed as the equal of the link. */
.desktop-offer-a button { border: 0; background: none; padding: 0;
                          font: inherit; font-size: 13px; color: var(--faint);
                          cursor: pointer; }
.desktop-offer-a button:hover { color: var(--ink); }

#chat-app[data-empty] #msgs     { flex: 0 1 auto; padding-bottom: 0;
                                  margin-top: auto; }
/* The block sits ABOVE true centre. The two auto margins split the free space
   evenly, so a fixed strip at the foot takes half of itself off the top and
   lifts the group: optical centre, not arithmetic centre, and the slack below
   is where the eye expects the conversation to start growing. rem and not vh,
   so the lift survives the side pane opening AND is the same nudge on a short
   window as on a tall one.

   9rem, not 5. This is now the ONLY thing lifting the group -- `.empty-mark`
   gave up its 9vh above -- and half of it is the lift, so the group rises
   4.5rem rather than 2.5rem. On a 900px window that puts the ring roughly a
   third of the way down instead of dead centre; on a short window the autos
   collapse first and the composer stays reachable, which is what the rem
   buys over another viewport unit. */
#chat-app[data-empty] #composer { margin-bottom: auto; padding-bottom: 9rem; }
/* Signed out there is no composer: #msgs is the pane's last child (it holds
   only the auth card), so it takes the bottom auto margin itself and the card
   sits centred by the same flexbox that centres the first-run home. */
#chat-app[data-empty] #msgs:last-child { margin-bottom: auto; }
/* Centred first-run home: the composer sits mid-viewport, so a menu opened
   upward (built for the pinned-bottom layout) runs off the top of the screen.
   All the free space is BELOW the centred composer; open the model menu and
   the document picker down there and cap them to that half. */
/* THE ROOM BELOW THE CENTRED COMPOSER IS NOT A CONSTANT, and the first
   version of this rule assumed it was. It capped at `min(420px, 44vh)`, which
   was measured once, on one greeting, and read as "44vh IS the room".

   It is not: the first-run greeting ROTATES, and a taller one pushes the
   composer down. Measured on the same 720px window, the space below the
   composer row is 44vh under "Il tuo prompt e al sicuro." and 38.2vh under
   "Chat verificata". A cap of 44vh therefore exceeds the room for most of the
   greetings, and the panel ends FLUSH with the window edge -- no bottom
   border, no shadow, no scrollbar end, nothing to say the list continues,
   which is exactly the state a reader reads as "this is all there is".

   Measured on one 720px window across three greetings, the room is 275px,
   271px and 252px: a two-line greeting costs a whole line. 35vh was set from
   the middle of that range and still landed flush on the tallest greeting.
   30vh clears the one-line-taller case as well, with a real gap left over.

   It is a constant standing in for a distance that is actually measurable,
   and that is a deliberate choice rather than an oversight: reading the
   composer's position in JS would be exact, and it would put a layout
   measurement and a resize listener into the composer to buy a panel two rows
   taller. The cost of the constant is those two rows; the panel scrolls, and
   the permanent slim scrollbar below is what says so. */
#chat-app[data-empty] #model-menu,
#chat-app[data-empty] #tools-menu,
#chat-app[data-empty] #effort-menu,
#chat-app[data-empty] #library-menu { bottom: auto; top: calc(100% + 6px);
                                      max-height: 30vh;
                                      max-height: 30dvh; }

/* Walled composer: the whole box acts as one button that re-opens the wall. The
   disabled send button dispatches no pointer events of its own, so it is taken
   out of hit testing and the click lands on the form, where the listener is. */
#composer.walled .composer-box { cursor: pointer; opacity: .72; }
#composer.walled .composer-row button,
#composer.walled .composer-row .model-wrap { pointer-events: none; }

/* Example cards: only on the empty state, never once a turn exists. They are
   pills now rather than three large squares, so the greeting and the composer
   stay the centre of gravity, but every one of them still demonstrates the
   vault (two attach a real sample document) and that is what they are for. */
#prompt-cards { display: none; }
#chat-app[data-empty] #prompt-cards { display: block; }
/* The kicker line names the cards and folds them. It carries the first-visit
   hint alongside, so the row keeps its top gap either way. */
.cards-kicker { font-family: var(--font-mono); font-size: 10px;
                letter-spacing: .08em; text-transform: uppercase;
                color: var(--graphite); font-weight: 500; margin: 18px 0 0;
                display: flex; align-items: center; gap: 10px;
                flex-wrap: wrap; }
/* A button that has to read as the label it replaced: every inherited text
   property back, none of the chrome. */
.cards-toggle { display: inline-flex; align-items: center; gap: 6px;
                font: inherit; color: inherit; letter-spacing: inherit;
                text-transform: inherit; background: none; border: 0;
                padding: 0; cursor: pointer; }
.cards-toggle:hover { color: var(--accent); }
.cards-toggle:focus-visible { outline: 2px solid var(--accent);
                              outline-offset: 3px; border-radius: 3px; }
.cards-caret { display: inline-grid; place-items: center;
               transition: transform .15s ease; }
/* Folded: the row is gone and the caret points at it. Folding the ROW, not
   #prompt-cards, is what keeps the label (and its button) on screen. */
#prompt-cards.folded .cards-row { display: none; }
#prompt-cards.folded .cards-caret { transform: rotate(-90deg); }
@media (prefers-reduced-motion: reduce) {
  .cards-caret { transition: none; }
}
.cards-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.prompt-card { display: inline-flex; align-items: center; gap: 9px;
               text-align: left; padding: 8px 14px 8px 10px;
               border: 1px solid var(--border); background: var(--surface);
               color: var(--ink-2); border-radius: 20px;
               font-family: var(--font-sans); font-size: 13px; cursor: pointer;
               box-shadow: var(--e1);
               transition: border-color .14s ease, color .14s ease,
                           transform .14s ease; }
.prompt-card:hover { border-color: var(--border-2); color: var(--ink);
                     transform: translateY(-1px); }
.prompt-card:focus-visible { outline: 2px solid var(--accent);
                             outline-offset: 2px; }
.card-title { font-size: 13px; font-weight: 500; line-height: 1.3; }
/* The sub-line named the sample document each card attaches. In a pill that
   sentence is the pill, so it moves to the title attribute the card already
   carries and stays out of the row. */
.card-sub { position: absolute; width: 1px; height: 1px; margin: -1px;
            padding: 0; overflow: hidden; clip: rect(0 0 0 0);
            white-space: nowrap; border: 0; }
/* The one card with no document behind it: an icon in the action navy rather
   than a file badge, because inventing a file type for an email would lie. */
.card-icon { display: grid; place-items: center; width: 22px; height: 22px;
             flex: 0 0 22px; border-radius: 7px; background: var(--accent-tint);
             color: var(--accent); }
.card-icon svg { width: 14px; height: 14px; }
/* The file badge shrinks with them: a pill is not tall enough for the large
   face the square cards used. */
.prompt-card .filebadge.lg { width: 17px; height: 21px; border-radius: 3px; }
.prompt-card .filebadge.lg b { font-size: 6px; padding-bottom: 3px; }
.prompt-card .filebadge.lg::after { border-width: 0 0 6px 6px; }

/* First-visit nudge: ChatSection adds .nudge once per browser (localStorage)
   and removes it on the first interaction. A soft pulse draws the eye to the
   examples without ever selecting or sending anything. */
.cards-hint { text-transform: none; letter-spacing: 0; color: var(--ok-ink);
              font-weight: 600; display: inline-block; }
#prompt-cards.nudge .prompt-card { animation: card-pulse 2s ease-in-out 3; }
#prompt-cards.nudge .prompt-card:nth-child(2) { animation-delay: .15s; }
#prompt-cards.nudge .prompt-card:nth-child(3) { animation-delay: .3s; }
@keyframes card-pulse {
  0%, 100% { box-shadow: var(--e1); }
  50% { box-shadow: 0 0 0 3px var(--ok-tint); border-color: var(--ok-line); }
}
@media (prefers-reduced-motion: reduce) {
  #prompt-cards.nudge .prompt-card { animation: none;
    border-color: var(--ok-line); }
}

/* ------------------------------------------------------------------ turns --- */
/* The asymmetry IS the layout. No avatars, no author labels, no timestamps
   above the text: who spoke is carried by the shape and the side. The ONE
   exception is the answered turn's vendor mark below: which company's model
   answered is a screening claim, not decoration. */
.turn { display: flex; flex-direction: column; margin-bottom: 30px; }
.turn.you { align-items: flex-end; }
.turn-avatar, .turn-author, .turn-time { display: none; }
.turn-main { min-width: 0; width: 100%; }
.turn.ai { flex-direction: row; gap: 12px; align-items: flex-start; }
.turn.ai .turn-main { flex: 1; }
/* The mark sits on the transcript, not in a chip: no white disc and no ring.
   The box was there to give the glyph a surface, but the transcript already
   has one, so all the disc did was outline the vendor logo in a second shape
   the vendor does not use, on the one page where every other mark (the file
   badges, the source marks) is drawn bare. The size stays: it is what keeps
   the answer's first line aligned with the bubble above it. */
/* No top margin any more: the head row beside it is 28px minimum and the mark
   is 28px, so the two line up on their own. The 2px was the offset that put a
   markless avatar next to the first line of a CARD, and the answer's first
   line is the model's name now. */
.turn-avatar.answer-mark { display: grid; place-items: center;
                           width: 28px; height: 28px; flex: none; }
.turn.you .turn-main { display: flex; flex-direction: column;
                       align-items: flex-end; }
/* The head carried an author name and a timestamp. Both are gone: the shape
   already says who spoke. What remains is the vault receipt, and it belongs
   UNDER the sentence it is about, not above it. */
.turn-head { display: none; }
/* WHO ANSWERED, above the answer and beside the mark that says the same thing
   (§ items 9 and 10). The head was `display: none` on both sides, because it
   used to carry "Cedreon" and a timestamp -- two things the shape of the turn
   already said. What it carries now is the vendor and the model, which the
   shape cannot say and which the reader wants BEFORE the paragraph rather
   than in the footer under it.
   28px minimum, matching the vendor mark exactly, so the two are centred on
   each other by construction rather than by a margin somebody measured once.
   The time stays hidden: it was hidden before this and nothing here changed
   its argument. */
.turn.ai .turn-head { display: flex; align-items: center; gap: 8px;
                      min-height: 28px; margin-bottom: 2px; }
.turn.ai .turn-head .turn-time { display: none; }
.turn.ai .turn-author { display: inline; font-size: 12px; font-weight: 600;
                        color: var(--graphite); letter-spacing: 0; }
/* The disclosure hangs off that line rather than sitting beside it as a
   second control: the summary IS the name, so there is one thing to read and
   one thing to press. It reuses `.model-card` (the header's ⓘ shape, which is
   where these two sentences used to live), so only the trigger is restyled --
   the popover keeps its own rule below. */
.turn-model { position: relative; min-width: 0; }
.turn-model summary { display: inline-flex; align-items: center;
                      width: auto; height: auto; padding: 2px 6px;
                      margin-left: -6px; border-radius: 7px; }
.turn-model summary:hover .turn-author,
.turn-model[open] summary .turn-author { color: var(--ink); }
.turn-model .cardinfo { font-size: 12px; }
/* Explicit order for the whole column, not just the two pieces that move: the
   masked-input fallback and the meta row are appended AFTER the card, and with
   only the card and the head ordered they floated to the top of the bubble as
   a right-aligned line with nothing to attach to. */
.turn.you .turn-main > * { order: 3; }
.turn.you .turn-card { order: 1; }
.turn.you .turn-head { display: flex; order: 2; align-items: center; }

/* What YOU said: warm, compact, right aligned. No shadow, because a bubble
   that floats reads as chrome and this is just your own sentence. */
.turn.you .turn-card { max-width: 82%; background: var(--paper);
                       border: 1px solid var(--hair); box-shadow: none;
                       border-radius: 18px 18px 5px 18px;
                       padding: 11px 15px; white-space: pre-wrap; }
/* The per-turn vault receipt: says WHEN the hiding happened, not just that it
   did. "before sending" is the whole promise, and this is the only place the
   turn itself makes it. */
.turn-vaulted { display: inline-flex; align-items: center; gap: 5px;
                margin-top: 8px; padding: 4px 10px; border-radius: 999px;
                background: var(--ok-tint); color: var(--ok-ink);
                font-size: 11.5px; font-weight: 500; }
/* As a button it must not arrive wearing the browser's chrome. A UA button
   brings `border: 2px outset`, Arial and `cursor: default`, none of which the
   rule above overrides -- so the receipt rendered as a beveled Arial box next
   to the flat pill it is meant to be, and the one thing it now needed to say,
   that it can be pressed, was the one thing the default cursor denied.
   Caught by looking at it: every assertion about this element passed while it
   looked like that, because they ask what it IS and not how it reads. */
button.turn-vaulted { appearance: none; -webkit-appearance: none; border: 0;
                      font-family: inherit; cursor: pointer; }
button.turn-vaulted:hover { background: var(--ok); color: #fff; }
button.turn-vaulted:focus-visible { outline: 2px solid var(--accent);
                                    outline-offset: 2px; }

.turn-card { font-size: 15px; line-height: 1.6; color: var(--ink);
             min-width: 0; overflow-wrap: break-word; }
/* What CEDREON said: no card at all. */
.turn.ai .turn-card { font-size: 15.5px; line-height: 1.72;
                      background: none; border: 0; box-shadow: none;
                      padding: 0; }
/* The exceptions, and only these: a refusal and an error are not answers,
   and a border is how the reader sees that at a glance. A stopped wait is
   the third and it is NOT red: nothing failed, somebody chose. */
.turn.ai .turn-card.blocked { border: 1px solid var(--bad);
                              background: var(--bad-tint);
                              border-radius: 12px; padding: 11px 14px; }
.turn.ai .turn-card.error { border: 1px solid var(--bad); color: var(--bad);
                            border-radius: 12px; padding: 11px 14px; }
.turn.ai .turn-card.notice { border: 1px dashed var(--hair);
                             color: var(--graphite); font-size: 13.5px;
                             border-radius: 12px; padding: 9px 14px; }
/* A reloaded turn's attached file, closed by default: the bubble shows the
   typed words and the chip, and this row is where the stored fold stays
   reachable (for some files it is the only surface that has it). Native
   details: the marker, the keyboard handling and the collapsed default come
   with the element. */
.turn.you .turn-fold { margin-top: 8px; font-size: 12.5px;
                       border-top: 1px solid var(--hair); padding-top: 6px; }
.turn.you .turn-fold summary { cursor: pointer; color: var(--graphite); }
.turn.you .turn-fold pre { margin: 6px 0 0; padding: 8px 10px;
                           border: 1px solid var(--hair); border-radius: 8px;
                           background: var(--paper);
                           max-height: 320px; overflow: auto;
                           white-space: pre-wrap; word-break: break-word;
                           font-size: 12px; }
/* A screened answer cannot stream, so it arrives whole. Settling it in is the
   product's actual behaviour, not decoration. */
@keyframes turn-settle { from { opacity: 0; transform: translateY(6px); }
                         to { opacity: 1; transform: none; } }
.turn.ai .turn-card { animation: turn-settle .42s cubic-bezier(.16,1,.3,1) both; }
@media (prefers-reduced-motion: reduce) {
  .turn.ai .turn-card { animation: none; }
}

/* A protected value is an ANNOTATION, not a badge: the sentence still reads as
   a sentence, and the mark says what the model was actually sent. The VALUE is
   a text node; the TOKEN is painted from data-tok by the ::after below, so
   selecting the sentence copies the words alone: a token in a text node rode
   every copy as "Nordwind Logistik⟨ORG_17⟩". Screen readers keep the pairing
   through the mark's aria-label. Anchored tight to the mark (bottom overlaps
   the line by 7px): floated a full line above, it read as belonging to
   nothing. */
/* A PLACEHOLDER IS SET APART FROM THE PROSE AROUND IT. Italic and a shade
   smaller: what stands here is not a word the writer chose, it is what the
   vault put in the place of one, and the sentence should read as having
   something quoted into it. Applied to the TOKEN only -- the revealed
   original below keeps the sentence's own face, so uncovering a value returns
   it to the prose rather than leaving it in the substitute's clothes. Size is
   relative (`em`) so a mark inside the panel's smaller rows scales with the
   row rather than fighting it, and the reveal geometry is measured at runtime
   (marks.tsx), so neither property can put §101.11's reservation out. */
mark.vault-hit { position: relative; background: linear-gradient(transparent 62%,
                 var(--ok-tint) 62%); color: inherit;
                 font-style: italic; font-size: .95em;
                 border-bottom: 1.5px solid var(--ok-line);
                 border-radius: 1px; padding: 0; cursor: help; }
.vault-hit .v-tok { position: absolute; left: 50%; bottom: calc(100% - 7px);
                    transform: translateX(-50%) translateY(3px);
                    background: var(--ink); color: var(--canvas);
                    font-family: var(--font-mono); font-size: 11px;
                    white-space: nowrap; padding: 5px 9px; border-radius: 7px;
                    box-shadow: var(--e2); opacity: 0; pointer-events: none;
                    user-select: none; -webkit-user-select: none;
                    transition: opacity .14s ease, transform .14s ease;
                    z-index: 30; }
.vault-hit .v-tok::before { content: "Sent as "; }
.vault-hit .v-tok::after { content: attr(data-tok); }
.vault-hit:hover .v-tok, .vault-hit:focus-visible .v-tok {
                    opacity: 1; transform: translateX(-50%) translateY(0); }
/* THE REVEAL IS ANCHORED, NOT IN FLOW (§100.1).

   Two designs have stood here. #287 drew a floating card holding the token,
   the words "Protected value" and the original -- three stacked things where
   the sentence had one, and the substitution left to the reader. §99.6
   replaced it with a SWAP: the mark printed the value in the token's own
   place, so the line read as prose either way.

   The swap is what is being reversed, and only for its side effect. A
   placeholder and its original are almost never the same width, so uncovering
   one resized the mark and moved every word after it -- measured, `⟨PERSON_1⟩`
   -> `Marco Rossi` was 4.1px and shifted both later marks by exactly that,
   and a longer value rewraps the paragraph. Because the shift happens under
   the pointer, a mark can grow out from beneath the cursor, fire `mouseleave`,
   retract and be hovered again: the message trembles, and easing cannot fix a
   loop whose input is its own output.

   So the token keeps its place and the value is drawn OUT OF FLOW against it.
   `position: absolute` inside the mark's own `position: relative`, so no
   ancestor is measured differently and nothing reflows; `pointer-events: none`
   is what closes the loop, because the pointer never leaves the mark it is
   pointing at. It is not a card: one string, no label, no repeated token --
   #287's real lesson, kept.

   It wraps rather than clipping. A vaulted value can be an address, and an
   address cut off mid-line is a way back that does not arrive; `width:
   max-content` keeps short values tight and `max-width` keeps a long one
   inside the pane on a phone.

   `user-select: none` is BACK, and for the reason §99.6 retired it: the value
   lies over a neighbouring line, so a selection dragged across the paragraph would
   sweep in text nobody aimed at, and copying a sent turn must reproduce what
   the PROVIDER saw. Reading is what a peek is for.

   The revealed MARK still fills the tint it only underlines while masked --
   same colour family, no second treatment -- so a paragraph with several
   uncovered marks still reads as a paragraph and not as highlighter. Both
   properties are paint, so the state change costs no layout either.

   `cursor: help`, never `pointer`: this is not a control (no role, no click
   handler), and `pointer` promises an action that pressing does not perform. */
/* The TOKEN's ink, not the token: it keeps its box, so nothing after it
   moves, and it stays a real selectable text node so a copy still reproduces
   what the provider saw (§91). `visibility: hidden` would satisfy the first
   and break the second -- browsers exclude hidden text from a copy. The value
   below sets its own colour, so it is unaffected. */
mark.vault-hit.revealed { background: var(--ok-tint);
                    border-bottom-color: var(--ok); color: transparent; }
/* The overlay carries the same tint, so the value and the word it stands in
   for read as one continuous highlighted run rather than two boxes. */
/* THE VALUE IS PAINTED OVER THE WORD'S OWN PLACE (§101.2).

   Two designs have covered other text here. §100.1's card sat ABOVE the mark
   and hid the line the reader was using to make sense of the word they were
   pointing at -- reported with a screenshot, a black panel reading
   `4485 2757 4451 0060` over the text above `⟨CARD_5⟩`. §101.4 moved it
   below, and measured on the same message it then covered the two marks on
   the next line. An out-of-flow card always covers something, and there is no
   free direction inside a paragraph.

   So the reveal is not a card. The token stays exactly where it is and keeps
   exactly the width it had -- only its INK goes transparent -- and the
   original is painted from the same left edge, on the same line, in the
   vault's own green. Nothing is measured and nothing reflows, which is what
   §100.1 was protecting; what it covers is at most a few words of the same
   sentence, immediately beside the value they belong to.

   `color: transparent` and never `visibility: hidden`: browsers exclude
   hidden text from a copy, and §91 requires that copying a sent turn
   reproduce the PROVIDER-facing placeholder. The token remains a real,
   selectable text node; the value is `user-select: none`, so it can never
   ride a copy.

   `pointer-events: none` is what closes §99.6's oscillation loop for good:
   the pointer stays on the mark it is pointing at, whatever is drawn over the
   words beside it, so the reveal can never move its own hit area. */
.vault-hit .vault-reveal { position: absolute; left: 50%; top: 0;
                    /* CENTRED ON THE TOKEN IT COVERS. It was pinned to the
                       mark's left edge, so a wide value grew rightwards only
                       and read as belonging to the words after the mark
                       rather than to the mark -- reported as "comincia quando
                       comincia ORG, invece deve apparire nel centro". Same
                       device `.v-tok` above already uses. */
                    transform: translateX(-50%);
                    width: max-content; max-width: min(320px, 62vw);
                    white-space: normal; overflow-wrap: anywhere;
                    background: var(--ok-tint); color: var(--ok-ink);
                    /* IT HAS TO FIT INSIDE THE BAND IT COVERS. The reveal is
                       out of flow, so it inherited the PARAGRAPH's line box
                       (24px) while the mark's own highlight is 20px tall:
                       measured, a green rectangle 4px taller than the word it
                       replaced, spilling above and below the line -- reported
                       as "the green replacement is too big". Both numbers are
                       relative so the one rule holds in the 15px transcript
                       and the 11px panel row alike. */
                    /* SMALLER THAN THE SENTENCE ON PURPOSE. It is an
                       aside about a word, not the word itself, and the
                       smaller it is the less room the line has to give up for
                       it -- so this number and the reservation below are one
                       decision. .86em is 12.9px in the 15px transcript and
                       9.5px in the 11px panel row, both still above the
                       smallest type the console sets anywhere. */
                    font-size: .86em; line-height: 1.3;
                    /* UPRIGHT, though the token it covers is italic. The
                       placeholder is set apart from the prose because it is a
                       substitute; the original is not a substitute, so
                       uncovering it returns the word to the sentence's own
                       face. Declared here rather than in a rule of its own:
                       one selector, one block. A second rule on this same
                       selector, earlier in the file, is what a reader -- and
                       test_vault_reveal's `_rule` -- finds first, and it
                       would describe none of the geometry below. (Nor may a
                       comment in this block carry a brace: the guard beside
                       it slices to the first closing one.) */
                    font-style: normal;
                    /* The 2px pull-back belongs to the EDGE-PINNED forms,
                       where the padding has to line up with the token's own
                       first letter. Centred there is nothing to line up with
                       and it would just be 2px off centre. */
                    border-radius: 2px; padding: 0 2px;
                    pointer-events: none;
                    user-select: none; -webkit-user-select: none;
                    /* ABOVE EVERYTHING IT COVERS. A reveal drawn UNDER a
                       neighbouring mark's own highlight, a chip or a sticky
                       panel head is a value the reader can see half of, which
                       is worse than not opening at all -- "la scritta va
                       sopra alle altre cose". */
                    z-index: 40;
                    animation: vault-reveal-in .12s ease-out both; }
/* THE ROOM THE SENTENCE GIVES UP WHILE A VALUE IS SHOWING (§101.11).
   In flow, so the words after the mark move aside and the value lands in a
   gap of its own rather than over its neighbours -- which an out-of-flow box
   can never do (§101.4: it always covers something). It carries no text, so
   it is `user-select: none` and out of hit testing; its width is set from the
   measured value and is never negative, so the mark can only ever GROW. */
.vault-hit .vault-pad { display: inline-block; height: 0;
                    pointer-events: none;
                    user-select: none; -webkit-user-select: none; }
/* NO EDGE-PINNED FORMS ANY MORE (§101.11). They kept an out-of-flow box
   inside its column; the mark now RESERVES the value's width, so the reveal
   centred on it always fits inside a box the layout has already placed, and
   where the line has no room the mark itself wraps like any other word. */
@keyframes vault-reveal-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* Motion is the decoration here and the reveal is the content, so the
   animation is dropped whole rather than shortened. */
@media (prefers-reduced-motion: reduce) {
  .vault-hit .vault-reveal { animation: none; }
}
/* A MARK THAT CANNOT OPEN SAYS SO. It keeps the vault's colour -- the value
   IS protected, which is the whole of what the mark reports -- and gives up
   the affordance: no underline to pull at, and the default cursor, because
   `help` on something that answers nothing is the promise that made "sometimes
   it works" a reasonable thing for a reader to conclude. */
.vault-hit.inert { border-bottom-color: transparent; cursor: text; }
.vault-hit:focus-visible { outline: 2px solid var(--ok-line);
                    outline-offset: 2px; border-radius: 2px; }

/* Inbound restored values are deliberately quieter than outbound ones: a reply
   full of restored names must never read as a wall of green. */
mark.vault-restored { background: transparent; color: inherit;
                      border-bottom: 1px dotted var(--ok-line); padding: 0; }
/* the side panel's "sent as ⟨TOKEN⟩" chips (and the demo film's) */
/* Same rule one surface over: a token printed in the panel is the same
   substitute the sentence carries, so it is set the same way. */
.tok-chip { font-family: var(--font-mono); font-size: 11.5px; font-style: italic;
            background: var(--accent-tint); color: var(--accent);
            border-radius: 5px; padding: 0 5px; white-space: nowrap; }

/* An attachment names itself on its own line above the text it came with,
   never inline in the middle of the sentence. */
.attach-tag { display: flex; width: fit-content; align-items: center; gap: 8px;
              font-size: 12.5px; color: var(--ink-2);
              background: var(--surface); border: 1px solid var(--border);
              border-radius: 9px; padding: 6px 10px 6px 7px; margin: 0 0 9px; }

/* per-turn footer rows */
.turn-meta { display: flex; flex-wrap: wrap; gap: 3px; align-items: center;
             font-size: 12px; color: var(--graphite); margin-top: 14px; }
.masked-toggle { border: 0; background: none; cursor: pointer;
                 padding: 5px 9px; border-radius: 7px;
                 display: inline-flex; align-items: center; gap: 5px;
                 font-family: var(--font-sans); font-size: 12px;
                 color: var(--ok-ink); font-weight: 500; }
.masked-toggle:hover { background: var(--ok-tint); }
.masked-fallback { font-family: var(--font-mono); font-size: 11.5px;
                   line-height: 1.72; white-space: pre-wrap;
                   word-break: break-word; background: var(--surface);
                   border: 1px solid var(--hair);
                   border-radius: 10px; padding: 12px 13px; margin-top: 8px;
                   color: var(--ink-2); }
.masked-fallback .cap { display: block; margin-bottom: 4px; }
.model-chip { font-size: 12px; color: var(--graphite); padding: 5px 9px; }
.vault-note { display: inline-flex; align-items: center; gap: 6px;
              padding: 5px 9px; border-radius: 7px; font-size: 12px;
              color: var(--ok-ink); }
/* As a BUTTON the receipt must not arrive wearing the browser's chrome, the
   same trap `button.turn-vaulted` records: a UA button brings `border: 2px
   outset`, its own font and `cursor: default`, and the rule above overrides
   none of them -- so a flat inline receipt renders as a beveled Arial box,
   and the one thing it now has to say, that it can be pressed, is the one
   thing the default cursor denies. Every static assertion passes while it
   looks like that, because they ask what the element IS and not how it reads.
   `background: none` matters too: `.vault-note` never set one, so the UA grey
   would arrive as a fill this chip has never had. */
button.vault-note { appearance: none; -webkit-appearance: none; border: 0;
                    background: none; margin: 0; font-family: inherit;
                    font-size: 12px; line-height: inherit; text-align: left;
                    cursor: pointer; }
button.vault-note:hover { background: var(--ok-tint); }
button.vault-note:focus-visible { outline: 2px solid var(--accent);
                                  outline-offset: 2px; }
.usage-note { font-family: var(--font-mono); font-size: 11px; padding: 5px 9px; }
.open-doc-btn { border: 0; background: none; color: var(--graphite);
                border-radius: 7px; font-size: 12.5px; font-weight: 500;
                padding: 5px 9px; cursor: pointer; font-family: var(--font-sans); }
.open-doc-btn:hover { background: var(--hover); color: var(--ink); }

/* Thinking indicator (replies are screened whole, never token streaming).
   The live animation is drawn by Chat/Orb.tsx; this shared sheet gives its
   canvas a box. Reduced motion is handled by the component as one still frame. */
.orb { display: inline-flex; align-items: center; padding: 3px 0 7px; }
.orb-canvas { display: block; width: 34px; height: 34px; }
/* Orb and words as one row: the orb alone says "busy", the words say how long
   this has been going on. */
.think-row { display: flex; align-items: center; gap: 12px; }
.think-row .orb { padding: 0; }
.think-label { font-size: 14px; color: var(--graphite); letter-spacing: .01em; }

/* ------------------------------------------------- the activity surface ---
   What Cedreon is doing, drawn as ONE vertical timeline under the message that
   started it, and gone again the moment an ordinary question is answered.

   ONE OBJECT, AND THE LIVE ROW IS ITS LAST. There used to be three things here
   at once -- a heading, a horizontal rail of chips, and a list under them --
   and most of this block was spent keeping them from saying the same word. A
   single list cannot collide with itself: the finished rows are above, the one
   running row is at the bottom, and where the reader has got to is how far
   down that row sits.

   THE SPINE IS THE ONE PIECE OF DRAWING, and it carries information rather
   than decorating. It runs from the first row's node to the live one and STOPS
   there -- nothing is drawn into empty space, because nothing has been
   promised about what comes next. Where the list is a tail, its topmost
   segment is DOTTED: solid means "you can see this", dotted means "there is
   more above, and here is how much".

   ONE THING MOVES. A list where every row breathes reads as a page that failed
   to load; a single pulse at the point of work reads as progress. Finished
   nodes are static characters at the weight of the surrounding text, the only
   continuous animation is the live node, and the only other motion is a short
   slide as a phrase changes or a row lands -- both one-shot, both removed by
   `prefers-reduced-motion`, which leaves a plain filled dot that still says
   where the work is.

   Green is the protection signal everywhere in this product (--ok), so the
   completed nodes wear it and nothing else here does. No card, no fill, no
   border around the list: it sits on the transcript's own ground exactly where
   the answer will replace it. */
.activity { margin: 2px 0 14px; min-width: 0; }
/* One row and no history: sized like the answer that replaces it, so nothing
   jumps when it does. */
.activity[data-mode="minimal"] { margin-bottom: 6px; }

.act-timeline { list-style: none; margin: 0; padding: 0; min-width: 0;
                display: flex; flex-direction: column; }

/* Each row owns the SEGMENT BELOW IT, which is what makes the line stop at the
   end of the list without anything having to know how long the list is. The
   LIVE row is the first child now, so the spine begins at the live node and
   descends through the past rather than climbing to meet it. */
.act-step { position: relative; display: flex; align-items: flex-start;
            gap: 10px; margin: 0; padding: 0 0 11px;
            animation: act-in .2s ease both; }
.act-step:last-child { padding-bottom: 0; }
.act-step::before { content: ""; position: absolute; left: 8px; top: 19px;
                    bottom: 1px; width: 1px; background: var(--border); }
.act-step:last-child::before { display: none; }
/* The row the elision counter hangs off yields its segment to the dotted one,
   so the gap is drawn once and says the truer thing about what follows. */
.act-step[data-tail="1"]::before { display: none; }
/* The agentic surface: a step naming a parent draws one level in, and takes
   its spine with it. */
.act-step[data-nested="1"] { padding-left: 17px; }
.act-step[data-nested="1"]::before { left: 25px; }

/* WHAT DID NOT FIT, COUNTED. The dotted segment is the honest half: the list
   really does continue past the last row drawn, and this says by how much
   instead of letting the reader assume it ends there. It draws ABOVE its own
   text -- the counter is the far end of the list now, and a dotted stub
   hanging off the bottom would be the spine drawn into empty space. */
.act-earlier { position: relative; margin: 0; padding: 11px 0 0 28px;
               font-size: 11.5px; color: var(--faint); line-height: 16px; }
.act-earlier::before { content: ""; position: absolute; left: 8px; top: 0;
                       height: 10px; width: 1px;
                       background-image: linear-gradient(var(--border-2) 50%,
                                                         transparent 0);
                       background-size: 1px 4px; background-repeat: repeat-y; }

.act-node { flex: none; width: 18px; line-height: 20px; text-align: center;
            font-size: 12px; color: var(--faint); }
.act-node-done { color: var(--ok); }
.act-node-failed { color: var(--bad); }
.act-node-running { display: inline-flex; align-items: center;
                    justify-content: center; height: 20px; }
/* THE ORB IS THE LIVE NODE (§84). At row scale it reads as one moving mark in
   the spine's own column rather than as a second animated object beside the
   words -- which is what it was when it sat next to a heading, a rail chip and
   a list row all describing the same wait. It sizes itself from this box. */
.act-node-running .orb { padding: 0; }
.act-node-running .orb-canvas { width: 18px; height: 18px; }

.act-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* Keyed on its own words in the component, so a change replaces the node and
   this plays once -- the transition is a consequence of the text moving rather
   than a second thing to keep in step with it. */
.act-label { font-size: 13.5px; color: var(--graphite); line-height: 20px;
             letter-spacing: .01em; overflow-wrap: anywhere;
             animation: act-phrase .24s ease both; }
.act-note { font-size: 12.5px; color: var(--faint); line-height: 17px; }
/* THE LIVE ROW IS THE ONE THE READER IS WAITING ON, so it is the one at full
   strength; everything BELOW it has already been read and steps back. */
.act-timeline > .act-step:first-child .act-label { font-size: 14px;
                                                   color: var(--ink); }
.act-failed .act-label { color: var(--bad); }
.act-skipped .act-label { color: var(--faint); text-decoration: line-through;
                          text-decoration-color: var(--border-2); }

/* A RUN'S OWN COUNT, and only a run's: an ordinary question does not invite
   "how far is this", so the line would be an itemised bill for work nobody
   commissioned. Aligned with the labels, not with the spine, because it is a
   summary of them rather than another step. */

@keyframes act-in { from { opacity: 0; transform: translateY(-3px); }
                    to   { opacity: 1; transform: none; } }
@keyframes act-phrase { from { opacity: 0; transform: translateX(6px); }
                        to   { opacity: 1; transform: none; } }

/* NOTHING SURVIVES THE ANSWER. A question's receipt is its reply; a turn that
   used a source already carries that source's citations; and a run has §80's
   own sealed record, which outlives a reload where this cannot. See
   `worthKeeping` in activity.ts -- the rule is enforced there, and this is the
   note for whoever comes looking for the styles that used to be here. */

@media (max-width: 640px) {
  /* The list is already the shape a narrow screen wants -- one column, one row
     per operation -- so nothing is hidden and nothing collapses into a
     stand-in. It only tightens. */
  .act-step { padding-bottom: 10px; }
  .act-label { font-size: 13px; line-height: 19px; }
  .act-timeline > .act-step:first-child .act-label { font-size: 13.5px; }
  .act-note { font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .act-step, .act-label { animation: none; }
  /* The orb draws ONE still frame under reduced motion; see Orb.tsx. */
}

/* --------------------------------------------------------------- markdown --- */
.md > :first-child { margin-top: 0; }
.md > :last-child { margin-bottom: 0; }
.md h1, .md h2, .md h3, .md h4 { font-family: var(--font-sans);
                                 font-weight: 640; letter-spacing: -.005em;
                                 margin: 24px 0 9px; line-height: 1.35;
                                 text-wrap: balance; }
.md h1 { font-size: 17px; } .md h2 { font-size: 16px; }
.md h3 { font-size: 15px; } .md h4 { font-size: 14.5px; }
.md p { margin: 0 0 15px; }
.md ul, .md ol { margin: 0 0 15px; padding-left: 19px;
                 display: flex; flex-direction: column; gap: 7px; }
.md li { margin: 0; }
.md li::marker { color: var(--faint); }
.md strong { font-weight: 600; }
.md blockquote { border-left: 2px solid var(--border-2); margin: 0 0 15px;
                 padding: 2px 0 2px 15px; color: var(--ink-2); }
.md hr { border: 0; border-top: 1px solid var(--border); margin: 22px 0; }
.md code { font-family: var(--font-mono); font-size: 12.5px;
           background: var(--hover); border-radius: 5px; padding: 1px 5px; }
.md a { color: var(--accent); }

.codeblock { border: 1px solid var(--hair); border-radius: 10px;
             overflow: hidden; margin: 0 0 15px; }
.codeblock-head { display: flex; align-items: center; gap: 8px;
                  padding: 4px 6px 4px 12px; background: var(--paper);
                  border-bottom: 1px solid var(--hair); }
.codeblock-lang { font-family: var(--font-mono); font-size: 10.5px;
                  letter-spacing: .05em; color: var(--graphite); flex: 1;
                  text-transform: lowercase; }
/* A produced document is an OBJECT in the conversation, not a paragraph of it,
   so the whole block sits on the one dark surface the product already owns.
   That is the entire separation: no new hue, no border trick, and it reads the
   same in both appearances because --code-bg moves with them. The head shares
   the surface under a thin white wash rather than flipping to a light bar,
   which is what made it look like two components stapled together. */
/* CONTRAST ONLY: the produced document goes fully dark, head included, so it
   reads as an object in the conversation rather than a paragraph of it. It is
   scoped to the one appearance that already carries a dark rail, where a
   second dark surface is a deliberate rhyme; dropped onto the all-warm page it
   would be a slab, and Navy is dark throughout already. */
:root[data-theme="contrast"] .codeblock { background: var(--code-bg);
                                          border-color: transparent; }
:root[data-theme="contrast"] .codeblock-head {
    background: rgba(255,255,255,.05);
    border-bottom-color: rgba(255,255,255,.07); }
:root[data-theme="contrast"] .codeblock-lang,
:root[data-theme="contrast"] .codeblock-file { color: var(--rail-muted); }
:root[data-theme="contrast"] .codeblock-title { color: var(--rail-ink); }
:root[data-theme="contrast"] .codeblock-head .mini-btn {
    border-color: rgba(255,255,255,.18); background: none;
    color: var(--rail-muted); }
:root[data-theme="contrast"] .codeblock-head .mini-btn:hover {
    border-color: rgba(255,255,255,.34); color: var(--rail-ink); }
/* The title takes the display face: a letter has a letterhead. The body stays
   mono because it is a draft meant to be copied out verbatim, which is honesty
   rather than costume. */
.codeblock.is-doc .codeblock-head { padding: 8px 8px 8px 14px; }
.codeblock-title { flex: 1; min-width: 0; font-family: var(--font-display);
                   font-size: 14px; font-weight: 600; color: var(--ink);
                   overflow: hidden; text-overflow: ellipsis;
                   white-space: nowrap; }
/* The one action a draft offers, bottom right: where a reader finishes, not
   where they start. Same surface as the body and NO rule above it: a hairline
   there read as a stray white line on the dark block, and the button's own
   border is separation enough. */
/* MESSAGE ACTIONS ARE QUIET BY DEFAULT (§ item 11). Copy sits on every code
   and document block in an answer, and a row of always-lit buttons over a
   long reply reads as chrome competing with the prose. Revealed on hover, and
   on `:focus-within` so the keyboard reaches what the pointer does.

   Scoped to `.turn.ai`: the same blocks appear in the side panel and in the
   document reader, where they are the surface's own controls rather than a
   message's, and nothing there is hovered to be used.

   `opacity`, not `visibility` or `display`: the button keeps its box, so the
   head does not resize under the pointer as it arrives. */
.turn.ai .codeblock-head .mini-btn,
.turn.ai .codeblock-foot .mini-btn { opacity: 0;
                                     transition: opacity .12s ease; }
.turn.ai:hover .codeblock-head .mini-btn,
.turn.ai:hover .codeblock-foot .mini-btn,
.turn.ai:focus-within .codeblock-head .mini-btn,
.turn.ai:focus-within .codeblock-foot .mini-btn { opacity: 1; }
/* A touch screen never hovers, so on one the reveal would be a control that
   does not exist. There, they are simply always there. */
@media (hover: none) {
  .turn.ai .codeblock-head .mini-btn,
  .turn.ai .codeblock-foot .mini-btn { opacity: 1; }
}
.codeblock-foot { display: flex; justify-content: flex-end;
                  padding: 0 8px 8px; background: var(--surface); }
:root[data-theme="contrast"] .codeblock-foot { background: transparent; }
:root[data-theme="contrast"] .codeblock-foot .mini-btn {
    border-color: rgba(255,255,255,.18); background: none;
    color: var(--rail-muted); }
:root[data-theme="contrast"] .codeblock-foot .mini-btn:hover {
    border-color: rgba(255,255,255,.34); color: var(--rail-ink); }
.codeblock-file { display: inline-flex; align-items: center; gap: 7px;
                  font-family: var(--font-mono); font-size: 11px;
                  color: var(--ink-2); font-variant-numeric: tabular-nums; }
.codeblock-file .filebadge { width: 15px; height: 18px; }
.codeblock-file .filebadge b { font-size: 6px; padding-bottom: 2px; }
.codeblock.is-doc code { line-height: 1.7; white-space: pre-wrap; }
/* The draft is edited where it is read. A focus ring INSIDE the block, so the
   caret has a visible home without the block jumping by a border's width.
   The offset must stay NEGATIVE (QA #45): a positive offset paints the ring
   outside the pre, and since the pre runs wall-to-wall inside a .codeblock
   that clips (overflow: hidden), the ring's left and right segments were cut
   away and only the top and bottom survived: two full-width near-black lines
   in --accent, laid over the letterhead above and the Copy foot below every
   time someone clicked into their email to change it. */
.codeblock.is-doc pre:focus { outline: 2px solid var(--accent);
                              outline-offset: -3px; border-radius: 6px; }
@media (max-width: 620px) {
  .codeblock-title { flex: 1 0 100%; margin-bottom: 2px; }
  .codeblock-head { flex-wrap: wrap; row-gap: 4px; }
}
.codeblock pre { margin: 0; padding: 12px 14px; overflow-x: auto;
                 background: var(--surface); }
.codeblock code { font-family: var(--font-mono); font-size: 12.5px;
                  line-height: 1.55; color: var(--ink); background: none;
                  padding: 0; white-space: pre; }
/* An unlabelled fence renders as plain text: no box, no head, wrapped, and
   in the PROSE face - the pre itself must drop the UA's mono default. */
.fence-plain { margin: 0 0 15px; font: inherit; white-space: pre-wrap; }
.fence-plain code { font: inherit; white-space: pre-wrap;
                    overflow-wrap: anywhere; background: none; padding: 0; }
/* Dark is the CONTRAST appearance's rhyme with its navy rail, never the
   warm page's: there the block was a slab, and a mail read as a terminal. */
:root[data-theme="contrast"] .codeblock pre { background: transparent; }
:root[data-theme="contrast"] .codeblock code { color: var(--code-ink); }
.table-scroll { overflow-x: auto; border: 1px solid var(--hair);
                border-radius: 10px; margin: 0 0 15px; }
.table-scroll table { margin: 0; min-width: 440px; }
.table-scroll th { background: var(--paper); border-bottom: 1px solid var(--hair);
                   padding: 9px 14px; }
.table-scroll td { padding: 10px 14px; border-bottom: 1px solid var(--hair);
                   color: var(--ink-2); }
.table-scroll tr:last-child td { border-bottom: 0; }
.table-scroll td:first-child { color: var(--ink); font-weight: 500; }

/* ---------------------------------------------------------------- composer --- */
/* The transcript scrolls UNDER the composer, so it must not end on a hard cut:
   the dock fades the canvas out above the card. */
#composer { padding: 6px 28px 20px; flex: 0 0 auto; position: relative;
            background: var(--canvas); }
#composer::before { content: ""; position: absolute; left: 0; right: 0;
                    bottom: 100%; height: 44px; pointer-events: none;
                    background: linear-gradient(to top, var(--canvas), transparent); }
#chat-app[data-empty] #composer::before { display: none; }
#composer > * { max-width: var(--measure); margin-left: auto; margin-right: auto; }
#deny-warning { display: flex; align-items: center; gap: 8px;
                background: var(--bad-tint);
                border: 1px solid var(--bad); color: var(--bad);
                border-radius: 12px; padding: 9px 13px; margin-bottom: 8px;
                font-size: 12.5px; font-weight: 500; }
#attach-chips { display: flex; flex-wrap: wrap; gap: 7px; }
#attach-chips:not(:empty) { margin-bottom: 8px; }
/* The attachment chips carry a real file-type badge (app.css .filebadge), so
   the padding starts tight on the left: the badge is the left edge. */
/* The composer is narrow while a document is open beside it, so a chip has to
   wrap within itself: one that only wraps between chips runs its own status
   line off the edge and under the pane. */
.attach-chip { display: inline-flex; align-items: center; gap: 7px;
               flex-wrap: wrap; max-width: 100%;
               font-size: 12px; color: var(--ink-2);
               background: var(--paper); border: 1px solid var(--hair);
               border-radius: 9px; padding: 5px 9px 5px 6px; }
.attach-chip.bad { color: var(--bad); border-color: var(--bad);
                   background: var(--bad-tint); }
/* A status line where the files are: an attach still in flight (a send waits
   for it rather than racing it) or one that has just landed. */
.attach-chip.wait { color: var(--graphite); }
.attach-chip .size { color: var(--faint); font-family: var(--font-mono);
                     font-size: 11px; }
/* A Word file the server kept in the library: asking for a change on this turn
   is what hands back a .docx with tracked changes. */
.attach-chip .edit-tag { color: var(--ok-ink); background: var(--ok-tint);
                         border-radius: 5px; padding: 1px 6px; font-size: 10px;
                         letter-spacing: .04em; text-transform: uppercase;
                         font-family: var(--font-mono); }
.attach-chip button { border: 0; background: none; cursor: pointer;
                      color: var(--faint); font-size: 12px; line-height: 1;
                      padding: 1px 3px; border-radius: 5px; }
.attach-chip button:hover { color: var(--ink); }

.composer-box { border: 1px solid var(--border); border-radius: 26px;
                background: var(--surface); padding: 14px 16px 10px;
                box-shadow: var(--e2);
                transition: border-color .15s ease, box-shadow .15s ease; }
.composer-box:focus-within { border-color: var(--border-2);
                             box-shadow: var(--e3); }
/* A file dragged anywhere over the chat, outlined the way the documents page
   outlines itself for the same gesture. The composer keeps its own border
   below: one says WHERE the drop will land, the other that it will be taken. */
#chat-app[data-dragging] { outline: 2px dashed var(--accent); outline-offset: -6px;
                           border-radius: 12px; }
#composer.drop-active .composer-box { border-color: var(--accent);
                                       background: var(--accent-tint); }
.input-wrap { position: relative; }
#hl-layer { position: absolute; inset: 0; pointer-events: none; overflow: hidden;
            white-space: pre-wrap; overflow-wrap: break-word;
            font: inherit; font-size: 15.5px; line-height: 1.6; padding: 0 2px;
            color: transparent; z-index: 0; }
#hl-layer mark { background: linear-gradient(transparent 62%,
                 var(--ok-tint) 62%); color: transparent;
                 box-shadow: 0 1.5px 0 var(--ok-line); }
#input { display: block; width: 100%; border: 0; outline: none; resize: none;
         background: transparent; font: inherit; font-size: 15.5px;
         line-height: 1.6; padding: 0 2px 4px; max-height: 200px;
         color: var(--ink); box-shadow: none; position: relative; z-index: 1;
         caret-color: var(--accent); }
#input::placeholder { color: var(--graphite); }
.composer-row { display: flex; align-items: center; gap: 4px; padding-top: 6px;
  /* The vault pill and Send were pushed off the right edge when the side
     panel narrowed the composer (found by driving the real page). Kept from
     main: this is behaviour, and the sizing above is appearance. #input must
     stay at #hl-layer's 15.5px/1.6 or the green marks drift off the text. */
  flex-wrap: wrap; row-gap: 8px; }
.spacer { flex: 1; }

/* Composer tools read as text with an icon, not as a row of outlined boxes:
   the card around them is already the control. */
#attach-btn, #model-trigger, #tools-trigger, #effort-trigger,
.composer-tool {
                 display: inline-flex; align-items: center; gap: 6px;
                 height: 32px; padding: 0 9px; border-radius: 8px;
                 border: 0; background: none; color: var(--ink-2);
                 cursor: pointer; font-size: 12.5px; font-weight: 500;
                 font-family: var(--font-sans); white-space: nowrap;
                 transition: background .12s ease, color .12s ease; }
#attach-btn:hover, #model-trigger:hover, #tools-trigger:hover,
#effort-trigger:hover, .composer-tool:hover {
                 background: var(--hover); color: var(--ink); }
/* An icon-only composer control that LATCHES (web search). Square, like the
   attach button beside it, and lit rather than outlined when it is on. It has
   to be legible as "on" at a glance and that is now the whole of its state:
   the chip that used to say it in words above the input is gone. Green,
   because a latched source is not the protection colour: `--accent-tint` is what this console uses for
   a chosen thing. */
.composer-tool { width: 32px; padding: 0; justify-content: center; }
/* THE STATE IS THE COLOUR, AND ONLY THE COLOUR. The button keeps its 32px
   square in both states -- no label grows out of it and the row never reflows
   when it is pressed, which is what makes it read as one control switching
   rather than the composer rearranging itself.

   NOT amber. The removed chip used `--warn-tint`, which this console keeps for
   a switch that reaches outside the building; this keeps `--accent-tint`, what
   it uses for a thing deliberately chosen. Recording the trade rather than
   hiding it: with the chip gone and no label here, nothing in the composer
   states IN WORDS that the question leaves as typed -- that sentence now lives
   only in the button's `title` and `aria-label`. */
.composer-tool.on { background: var(--accent-tint); color: var(--accent); }
.composer-tool.on:hover { background: var(--accent-tint); color: var(--accent); }
#tools-trigger[aria-expanded="true"],
#effort-trigger[aria-expanded="true"] { background: var(--hover); color: var(--ink); }
.tools-caret { opacity: .6; }
#attach-btn { width: 32px; padding: 0; justify-content: center; }
.icon-btn#attach-btn { border: 0; background: none; }

.model-wrap { position: relative; }

/* EFFORT, and only for a model pinned by hand that the gateway says takes one.
   It is NOT a permanent composer control: the trigger appears next to the
   model trigger when a slug with an effort knob is selected and is absent
   otherwise, so an ordinary Auto turn keeps the composer row it always had.

   A TRIGGER PLUS A PANEL, not a row of segments. Six equal-weight buttons made
   an ORDERED thing look like a set of alternatives, and they could not carry a
   mark, a scale or the rung Auto resolves to without growing wider than the
   composer. The panel opens downward like the model menu and scrolls with it. */
.effort-wrap { position: relative; }
/* The trigger takes the rule above with attach, model and Sources rather than
   a shape of its own. It shipped as a bordered white pill and read as an INPUT
   dropped into the row -- a second card inside the card that already is the
   control, and the only boxed thing on a row of four. Being the one control
   that comes and goes made that worse, not better: the box arrived with it. */
#effort-trigger:focus-visible { outline: 2px solid var(--brand);
                                outline-offset: 1px; }
/* The panel. Anchored and capped exactly like the model menu, including the
   first-run rule further up that flips it downward from a centred composer:
   two panels on one row that disagreed about which way they open is a seam a
   reader notices immediately. */
#effort-menu { position: absolute; bottom: calc(100% + 9px); left: 0; z-index: 50;
               width: 236px; max-width: calc(100vw - 32px);
               background: var(--surface); border: 1px solid var(--border);
               border-radius: 13px; box-shadow: var(--e3); padding: 11px 12px 9px;
               max-height: calc(100vh - 150px);
               max-height: calc(100dvh - 150px); overflow-y: auto;
               overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.effort-head { display: flex; align-items: baseline; gap: 7px; }
.effort-kicker { font-family: var(--font-mono); font-size: 10px;
                 letter-spacing: .07em; text-transform: uppercase;
                 color: var(--faint); }
.effort-current { font-size: 13.5px; font-weight: 600; color: var(--ink); }
/* One sentence on a title, never a second layer to dismiss. */
.effort-help { margin-left: auto; width: 15px; height: 15px; flex: none;
               display: grid; place-items: center; border-radius: 50%;
               border: 1px solid var(--border-2); color: var(--faint);
               font-size: 9.5px; cursor: help; }
/* The two ends written out. This is what makes the list read as a SCALE: the
   rung names alone say which is which only if you already know the ladder. */
.effort-ends { display: flex; justify-content: space-between; gap: 8px;
               margin: 9px 0 5px; font-size: 10.5px; color: var(--faint); }
.effort-track { display: flex; flex-direction: column; gap: 1px; }
.effort-stop { display: flex; align-items: center; gap: 9px; width: 100%;
               padding: 6px 7px; border: 0; border-radius: 8px;
               background: none; color: var(--ink-2); font: inherit;
               font-size: 13px; text-align: left; cursor: pointer;
               transition: background .12s ease, color .12s ease; }
.effort-stop:hover { background: var(--hover); color: var(--ink); }
.effort-stop.is-selected { background: var(--accent-tint); color: var(--ink); }
.effort-stop.is-selected .effort-stop-name { font-weight: 600; }
.effort-stop:focus-visible { outline: 2px solid var(--brand);
                             outline-offset: -2px; }
.effort-stop-name { flex: 1; min-width: 0; }
/* What Auto resolves to on THIS model, printed rather than hidden in a tooltip:
   "Auto" alone is the one rung whose meaning changes per model. */
.effort-stop-note { font-family: var(--font-mono); font-size: 10px;
                    color: var(--faint); flex: none; }
/* The rung's gauge. ONE dial, five needle positions: the angle says how hard
   the rung is on the shared ladder, so a model that skips a rung shows the gap
   as a jump. It is a stroked mark on `currentColor` like every other composer
   glyph, so it inherits the muted ink and lifts with the row on hover. */
.effort-mark { flex: none; color: inherit; }
/* The arc is the context and the needle is the value, so the dial sits back a
   step. One colour still: a second hue would make a level control look like a
   status light. */
.effort-mark .dial { opacity: .42; }
.effort-stop.is-selected .effort-mark { color: var(--brand); }
/* Under ~760px the composer row cannot hold two labelled triggers beside the
   attach button and Sources. The WORD goes, the trigger never does: the mark
   alone still says which rung is set, and an effort a thumb cannot reach is
   the actual loss. */
@media (max-width: 760px) {
  #effort-label { display: none; }
  #effort-trigger { padding: 0 7px; }
}
/* The composer is inert while the free allowance is spent, and the effort
   control follows the model picker rather than staying live beside it. */
#composer.walled .composer-row .effort-wrap { pointer-events: none;
                                              opacity: .55; }
/* The menu opens UPWARD from the composer, so its height is bounded by the
   window, not by the list: four vendor groups of two-line options run taller
   than a laptop viewport, and the top of the list (Auto, the recommended one)
   was scrolling off screen with no way to reach it. */
#model-menu { position: absolute; bottom: calc(100% + 9px); left: 0; z-index: 50;
              width: 300px; max-width: calc(100vw - 32px); background: var(--surface);
              border: 1px solid var(--border); border-radius: 13px;
              box-shadow: var(--e3); padding: 5px;
              max-height: calc(100vh - 150px);
              max-height: calc(100dvh - 150px); overflow-y: auto;
              overscroll-behavior: contain;
              /* Momentum scrolling on iOS, and never a scroll chained to the
                 page behind: the composer menus are the one thing on this
                 screen a thumb scrolls that is not the transcript. */
              -webkit-overflow-scrolling: touch; }
/* macOS hides overlay scrollbars until something is already scrolling, which
   is exactly backwards for a menu: the reader has to know the list continues
   BEFORE they decide to scroll it. These three are the only surfaces here that
   are both short and long -- a small box holding more than it can show -- so
   they get a permanent slim track. The transcript keeps the overlay
   behaviour; a scrollbar that is always drawn beside prose is noise. */
/* The thumb is `--graphite` and NOT a border token, which is the whole point:
   `--border` is a hairline at 1.29:1 against the white surface and 1.25:1
   against the dark one, so a scrollbar drawn in it is a scrollbar nobody sees
   -- the defect this rule exists to fix, restated in a colour. `--graphite`
   measures 5.05:1 light and 5.17:1 dark, near enough symmetric to need no
   per-theme override. Both spellings ship: modern Chrome honours the standard
   pair and ignores the -webkit- pseudo-elements once `scrollbar-width` is set,
   Safari and older Chrome do the reverse, so the two must agree on colour. */
#model-menu, #tools-menu, #library-menu, #effort-menu { scrollbar-width: thin;
              scrollbar-color: var(--graphite) transparent; }
#model-menu::-webkit-scrollbar,
#tools-menu::-webkit-scrollbar,
#library-menu::-webkit-scrollbar,
#effort-menu::-webkit-scrollbar { width: 10px; }
#model-menu::-webkit-scrollbar-track,
#tools-menu::-webkit-scrollbar-track,
#library-menu::-webkit-scrollbar-track,
#effort-menu::-webkit-scrollbar-track { background: transparent; }
#model-menu::-webkit-scrollbar-thumb,
#tools-menu::-webkit-scrollbar-thumb,
#library-menu::-webkit-scrollbar-thumb,
#effort-menu::-webkit-scrollbar-thumb { background: var(--graphite);
              border: 3px solid var(--surface); border-radius: 999px; }
#model-menu::-webkit-scrollbar-thumb:hover,
#tools-menu::-webkit-scrollbar-thumb:hover,
#library-menu::-webkit-scrollbar-thumb:hover,
#effort-menu::-webkit-scrollbar-thumb:hover { background: var(--ink-2); }
.menu-kicker { font-family: var(--font-mono); font-size: 10px;
               letter-spacing: .07em; text-transform: uppercase;
               color: var(--faint); padding: 7px 9px 5px; }
/* One vendor: its header and its rows. The wrapper exists for `position:
   sticky` and nothing else, so it stays a plain block -- every selector here
   reaches options as DESCENDANTS of #model-menu, never as children. A sticky
   box is released by its CONTAINING BLOCK, and while the headers were flat
   siblings that block was the whole menu: none of them was ever pushed out by
   the next, so they stacked on the same pixel at the same z-index and the one
   that painted was whichever came last in the DOM. Scrolling through the
   Anthropic rows showed a header reading "Qwen". */
.menu-section { display: block; }
/* Vendor group header. Sticks to the top of the scrolling list so the mark and
   the name stay with the rows they label; top is -5px, not 0, to sit under the
   menu's own padding rather than letting rows show through above it. */
.menu-group { display: flex; align-items: center; gap: 6px;
              position: sticky; top: -5px; z-index: 1;
              background: var(--surface);
              font-family: var(--font-mono); font-size: 10px;
              letter-spacing: .07em; text-transform: uppercase;
              color: var(--faint); padding: 8px 9px 4px; }
/* The mark carries its own width/height attributes (_vendor_icons.html) and
   this rule only positions it. Deliberate: sizing an inline SVG in CSS alone
   means a stale stylesheet renders it at the container's full width, which is
   what turned these into full-page artwork once. */
.vendor-icon { display: block; flex: none; }
/* The generic mark (composer.tsx VendorIcon): a vendor whose official artwork
   is not cleared to ship yet. It must not read as branding, so it takes the
   header's own muted colour and none of its own. */
.vendor-icon.is-generic { color: var(--faint); }
#model-menu [role="option"] { display: block; width: 100%; text-align: left;
                              border: 0; background: none; border-radius: 9px;
                              padding: 8px 10px; cursor: pointer; font: inherit;
                              transition: background .12s ease; }
#model-menu [role="option"]:hover, #model-menu [role="option"].focused
              { background: var(--hover); }
#model-menu [role="option"][aria-selected="true"] { background: var(--accent-tint); }
.opt-name { display: block; font-size: 13.5px; font-weight: 500; color: var(--ink); }
#model-menu [role="option"][aria-selected="true"] .opt-name { font-weight: 600; }
.opt-name em { font-style: normal; font-family: var(--font-mono);
               font-size: 9.5px; font-weight: 600; letter-spacing: .05em;
               border-radius: 999px; padding: 1px 7px; margin-left: 6px;
               vertical-align: 1px; }
.opt-name em.ok { background: var(--ok-tint); color: var(--ok-ink); }
/* One line, always. The tier copy is short enough to fit (console._TIER_DESC),
   and the clamp is the guard: a two-line description is what made the picker
   taller than the viewport, and the wrap point moved with every label edit. */
.opt-desc { display: block; font-size: 11px; color: var(--graphite);
            margin-top: 1px; white-space: nowrap; overflow: hidden;
            text-overflow: ellipsis; }

/* The Tools menu: one surface for everything the next message may reach for,
   mirroring the model menu's anchoring so the composer stays two controls
   wide however many tools land here later. */
.tools-wrap { position: relative; }
#tools-menu { position: absolute; bottom: calc(100% + 9px); left: 0; z-index: 50;
              width: 288px; background: var(--surface);
              border: 1px solid var(--border); border-radius: 13px;
              box-shadow: var(--e3); padding: 5px;
              max-height: calc(100vh - 150px);
              max-height: calc(100dvh - 150px); overflow-y: auto;
              overscroll-behavior: contain; }
.tools-kicker { display: block; padding: 8px 10px 4px;
                font-family: var(--font-mono); font-size: 10px;
                letter-spacing: .07em; text-transform: uppercase;
                color: var(--faint); }
.tool-row { display: flex; align-items: center; gap: 10px; width: 100%;
            padding: 7px 9px; border: 0; border-radius: 9px; background: none;
            color: var(--ink); font: inherit; text-align: left;
            cursor: pointer; }
.tool-row:hover { background: var(--hover); }
.tool-ico { display: grid; place-items: center; width: 22px; flex: none;
            color: var(--graphite); }
.tool-text { flex: 1; min-width: 0; display: flex; flex-direction: column;
             line-height: 1.3; }
.tool-text b { font-size: 13px; font-weight: 600; }
.tool-sub { font-size: 11.5px; color: var(--faint); }
.tool-state { flex: none; font-family: var(--font-mono); font-size: 10px;
              letter-spacing: .06em; text-transform: uppercase;
              color: var(--faint); }
/* An engaged source keeps the console's meaning of green: grounded, screened,
   stayed private. The web row is the one exception below. */
.tool-row[aria-checked="true"] .tool-ico { color: var(--ok); }
.tool-row[aria-checked="true"] .tool-state { color: var(--ok); }
/* Deliberately NOT the library row's green. Green is this console's "stayed
   private" colour; the one switch that sends the question to a third party
   outside the EU must not borrow it. Amber is the usage meter's register. */
/* The web row's amber-when-on rules are gone with the row: `#web-toggle` is a
   glyph in the composer now, it reports `aria-pressed` rather than
   `aria-checked`, and it carries neither `.tool-ico` nor `.tool-state`. Its
   lit state is `.composer-tool.on` above. */
/* Drive is the second switch that sends the question out of the building, so it
   keeps the web row's amber for the same reason and never the library's green.
   Its icon is a brand mark with fixed fills, so only the word can carry state. */
#drive-toggle[aria-checked="true"] .tool-state { color: var(--warn); }
/* The slashed shield on the trigger itself, drawn only while automatic
   detection is off. It sits AFTER the caret so the button's own words keep
   their place and nothing reflows when the state changes; `#tools-label` is
   hidden on a narrow screen and this must survive that, so it is not attached
   to the label. */
.tools-flag { display: inline-flex; align-items: center; margin-left: 5px;
              color: var(--warn); }
.tools-flag svg { display: block; }

/* Automatic detection inverts the two rows above, so it inverts their colour
   rule as well. There, amber marks the switch that is ON because that is when
   the question leaves. Here amber marks the switch that is OFF, because that
   is when names reach the provider as typed. Green would be the wrong promise
   in both directions: this row never means "stayed private", it means
   "stopped guessing". */
#autovault-toggle[aria-checked="false"] .tool-ico { color: var(--warn); }
#autovault-toggle[aria-checked="false"] .tool-state { color: var(--warn); }
/* ...and it must not pick up the generic green the shared rule below grants
   every checked row, for the same reason. */
#autovault-toggle[aria-checked="true"] .tool-ico { color: var(--graphite); }
/* The connect state is an ACTION, not a switch that is off. Set in the accent
   so it reads as the one row in this menu that leads somewhere. */
#drive-toggle:not([aria-checked]) .tool-state { color: var(--accent); }
.lib-count { font-family: var(--font-mono); font-size: inherit;
             font-style: normal; color: inherit; }
.tool-row-wrap { display: flex; align-items: center; gap: 2px; }
.tool-row-wrap .tool-row { flex: 1; min-width: 0; }
#library-choose { display: inline-flex; align-items: center;
                  justify-content: center; width: 24px; height: 28px; flex: none;
                  border: 0; border-radius: 7px; background: none;
                  color: var(--ink-2); cursor: pointer; }
#library-choose:hover { background: var(--hover); color: var(--ink); }

/* Active sources ride their own chip line above the input, each removable:
   what the next send will do, read in the same frame it is typed in. */
.tool-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.tool-chips[hidden] { display: none; }
.tool-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px;
             padding: 0 6px 0 10px; border-radius: 999px;
             background: var(--ok-tint); color: var(--ok-ink);
             font-size: 11.5px; font-weight: 600; }
/* THERE IS NO `.tool-chip.web`. The web chip left this strip when the globe
   grew its own label, and the rule went with it rather than being kept warm:
   an unused component in a stylesheet that promises the product's real markup
   is a component that lies -- the same reason the "Soon" rail style was
   deleted rather than parked. `--warn-tint` is still what Drive's chip uses.
   */
/* The Drive chip carries the connector's own mark, which is what a person
   recognises before they read anything. Amber like the web chip and never the
   library's green: the question is answered from files here, but it still
   leaves for Google to find them. The mark keeps its brand colours (it is a
   vendor mark, like the file badges), so the tint has to stay quiet enough not
   to fight it, and the padding closes up on the left because a 12px mark is
   already an inset. */
.tool-chip.drive { background: var(--warn-tint); color: var(--warn);
                   padding-left: 7px; }
.tool-chip .chip-mark { display: grid; place-items: center; width: 13px;
                        height: 13px; flex: none; }
.tool-chip .chip-mark svg { display: block; width: 13px; height: 12px; }
.tool-chip .chip-label { border: 0; background: none; color: inherit;
                         font: inherit; padding: 0; cursor: pointer;
                         display: inline-flex; align-items: center; gap: 6px;
                         min-width: 0; }
/* Open the document beside the chat, from a LIBRARY chip. Deliberately the
   same shape as `.attach-chip .attach-open`, because it is the same door: an
   outline pill that reads as a control next to a label that is not one. It
   inherits the chip's green rather than the attach chip's neutral border, so
   it belongs to the source strip it sits in instead of looking pasted in. */
.tool-chip .chip-open { border: 1px solid currentColor; background: none;
                        color: inherit; font: inherit; font-size: 10.5px;
                        font-weight: 600; padding: 0 7px; line-height: 16px;
                        border-radius: 999px; cursor: pointer; opacity: .72;
                        white-space: nowrap; flex: none; }
.tool-chip .chip-open:hover { opacity: 1; }
.tool-chip .chip-label .filebadge { width: 13px; height: 16px; flex: none; }
.tool-chip .chip-label .filebadge b { font-size: 5px; padding-bottom: 2px; }
.tool-chip .chip-label .filebadge::after { border-width: 0 0 5px 5px; }

/* WHAT DID CEDREON PROTECT FOR THIS REPLY? -- the panel's first-level answer.
   One headline, one split under it, the vocabulary behind a disclosure. It
   replaced four differently-worded true sentences the reader had to reconcile
   ("20 source values protected", "In this draft: none", "Protected in sources
   for last reply: 20 values", "Known vault value: 20"), so its whole job is to
   look like ONE thing: a single card, at the weight of a section head, not a
   stack of rows. */
.reply-protection { margin: 2px 0 14px; padding: 10px 12px;
                    border: 1px solid var(--line-2); border-radius: 10px;
                    background: var(--surface-2, transparent); }
.reply-protection > summary.reply-protection-head {
  display: flex; align-items: center; gap: 8px; list-style: none;
  cursor: pointer; color: var(--ink); font-size: 13px; }
.reply-protection > summary::-webkit-details-marker { display: none; }
.reply-shield { color: var(--good, var(--ink-2)); font-size: 13px; flex: none; }
.reply-protection-head b { font-weight: 600; }
/* Level 2: the two origins, side by side, because the question the headline
   raises is which of them the number came out of. Both drawn at zero. */
.reply-split { display: flex; gap: 18px; margin: 10px 0 0; flex-wrap: wrap; }
.reply-split-cell { display: flex; align-items: baseline; gap: 6px; }
.reply-split-label { color: var(--ink-3); font-size: 11.5px; }
.reply-split-cell b { color: var(--ink); font-size: 13px; font-weight: 600;
                      font-variant-numeric: tabular-nums; }
/* Level 3: everything that is true and is not the first question. */
.reply-detail { margin: 10px 0 0; }
.reply-detail > summary { cursor: pointer; color: var(--ink-3); font-size: 11.5px; }
.reply-detail .source-vault-note:last-child { margin-bottom: 0; }

.source-vault-types { display: grid; gap: 6px; margin: 8px 0; }
.source-vault-row { display: flex; align-items: center; justify-content: space-between;
                    gap: 16px; color: var(--ink-2); font-size: 12px; }
.source-vault-row b { color: var(--ink); font-variant-numeric: tabular-nums; }
.source-vault-note { margin: 8px 0 14px; color: var(--ink-3); font-size: 11.5px;
                     line-height: 1.45; }

/* Provenance: which file each protected value came out of. One block per
   attachment, so two documents never read as one pile. The token is mono
   because it is an identifier the reader may search the transcript for. */
.attachment-vault { margin: 8px 0 12px; }
.attachment-vault-head { display: flex; align-items: center; gap: 8px;
                         margin-bottom: 6px; }
/* `.filename` owns the clipping now and keeps the end of the name (§100.2):
   this row names WHICH document a block of protected values came from, and
   two files from one matter used to arrive here with the same visible name. */
.attachment-vault-name { flex: 1; min-width: 0;
                         color: var(--ink); font-size: 12.5px; font-weight: 500; }
/* WHERE THE FILE CAME FROM (§101.7), said on the row because it used to be
   said by a second heading. Quiet, and absent on an attachment: a tag on every
   row is a column of noise, and the ordinary case does not need naming. */
.attachment-vault-from { color: var(--ink-3); font-size: 11px;
                         font-style: italic; white-space: nowrap;
                         flex: none; }
.attachment-vault-count { color: var(--ink-3); font-size: 11.5px;
                          font-variant-numeric: tabular-nums; white-space: nowrap; }
/* What the SENDER marked, beside what the detectors found: two facts about the
   same file, so they share the row (§63). The separator is drawn rather than
   written into the string, because the phrase before it is one sentence in
   three languages and the phrase after it is another. */
.attachment-vault-added::before { content: "\00B7"; margin: 0 5px;
                                  color: var(--line-2); }
/* THE HEAD IS THE TOGGLE AGAIN, AND ONLY WHERE THERE IS SOMETHING BEHIND IT
   (§101.5). §101.1 made it a plain row -- correctly, at the time: it opened
   onto a token list the ledger below was already showing, so the disclosure
   bought nothing. §101.2 gave the block its OWN list, one row per placeholder
   this file minted, and with two attached documents that is twenty rows above
   a ledger they were subtracted from. So the line opens and closes its own
   list, and a block with nothing to disclose keeps the plain row and,
   deliberately, no marker and no pointer: a row that cannot be opened must
   not look as if it can. */
details.attachment-vault > summary.attachment-vault-head {
                         cursor: pointer; list-style: none; }
details.attachment-vault > summary::-webkit-details-marker { display: none; }
/* THE MARKER'S COLUMN BELONGS TO EVERY ROW, AND ONLY SOME ROWS DRAW IN IT.
   The caret used to exist solely on the `summary` form, so a row with nothing
   to disclose -- a staged attachment, a source that minted no token -- lost
   the caret AND its width, and its badge, name and count all slid one column
   left of every neighbour that had one. Reported as the new document landing
   "mal posizionato": two rows in one list, on two different left edges.

   So the box is declared on the head itself and reserved on both forms; the
   plain row hides the GLYPH and keeps the space. `visibility`, not `display`
   or a bare `content: ""`, because it is the same box either way and that is
   the whole point -- nothing here has to know the caret's glyph width, which
   is what a hand-tuned indent would have had to guess. A row that cannot be
   opened still shows no marker and no pointer (§101.5). */
.attachment-vault-head::before {
                         content: "\203A"; font-family: var(--font-mono);
                         font-size: 12px; line-height: 1; color: var(--ink-3);
                         flex: 0 0 7px; text-align: center;
                         transition: transform .12s ease; }
div.attachment-vault-head::before { visibility: hidden; }
details.attachment-vault[open] > summary::before { transform: rotate(90deg); }
details.attachment-vault > summary:hover .attachment-vault-name {
                         text-decoration: underline;
                         text-underline-offset: 2px; }
details.attachment-vault > summary:focus-visible {
                         outline: 2px solid var(--accent); outline-offset: 2px;
                         border-radius: 6px; }
/* The per-document receipt: a file's own slice of the protected outgoing
   text, same marking as the conversation disclosure, cut to the file the
   reader opened. */
.file-saw { margin: 4px 0 2px; }
.file-saw > summary {
  display: inline-block; list-style: none; cursor: pointer;
  color: var(--ink-3); font-size: 11px; text-decoration: underline;
  text-underline-offset: 2px; }
.file-saw > summary::-webkit-details-marker { display: none; }
.file-saw > summary:hover { color: var(--ink); }
/* What the fold contains, said inside it. The summary is the CONTROL
   (`Show details`) and this is the HEADING (`What the model saw`): the fold
   used to carry the heading on the summary itself, which named one of the two
   and left the other unsaid. */
.file-saw-label { margin: 0 0 5px; font-family: var(--font-sans);
                  font-size: 11px; font-weight: 500; letter-spacing: .02em;
                  text-transform: uppercase; color: var(--ink-3); }
.file-saw-body {
  margin: 5px 0 6px; padding: 9px 11px; background: var(--surface);
  border: 1px solid var(--hair); border-radius: 8px;
  font-family: var(--font-mono); font-size: 11px; line-height: 1.65;
  color: var(--ink-2); overflow-wrap: break-word;
  white-space: pre-wrap; max-height: 300px; overflow: auto; }
.file-saw-body b {
  color: var(--ok-ink); font-weight: 500; background: var(--ok-tint);
  border-radius: 3px; padding: 0 2px; }
.token-rows { display: grid; gap: 5px; margin: 6px 0 0; }
.token-row { display: flex; align-items: center; justify-content: space-between;
             gap: 12px; font-size: 12px; color: var(--ink-2); }
.token-row-token { font-family: var(--font-mono); font-size: 11px; font-style: italic;
                   color: var(--ink);
                   overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-x { display: grid; place-items: center; width: 16px; height: 16px;
          border: 0; border-radius: 50%; background: none; color: inherit;
          opacity: .65; cursor: pointer; padding: 0; font-size: 9px;
          line-height: 1; }
.chip-x:hover { opacity: 1; background: rgba(0,0,0,.08); }

/* A workflow tool opens a step, not a permanent toggle. */
.flow-strip { display: flex; align-items: center; gap: 10px;
              margin-bottom: 8px; padding: 8px 10px; border-radius: 11px;
              background: var(--paper); border: 1px solid var(--border); }
.flow-strip[hidden] { display: none; }
.flow-ring { width: 11px; height: 11px; flex: none;
             border: 3px solid var(--accent); border-radius: 50%; }
.flow-text { flex: 1; min-width: 0; display: flex; flex-direction: column;
             line-height: 1.3; }
.flow-text b { font-size: 12.5px; font-weight: 600; }
.flow-sub { font-size: 11.5px; color: var(--faint); }

/* The document picker: which documents a grounded turn may draw from.
   Anchored like the tools menu, above the composer. */
/* WIDE ENOUGH TO TELL TWO FILES APART (§100.2). 260px left `.lib-doc-name`
   207 of them, which is `AI Adoption in European Govern` and an ellipsis --
   the same string for every document in a series. The menu is what this list
   is FOR, so it takes the width it needs and stops at the composer's own,
   which is where a menu that overhangs its trigger starts looking detached.
   `.filename` keeps each name's end regardless; this is what stops there
   being nothing but an end. */
#library-menu { position: absolute; bottom: calc(100% + 9px); left: 0; z-index: 50;
                width: min(420px, calc(100vw - 48px)); background: var(--surface);
                border: 1px solid var(--border); border-radius: 13px;
                box-shadow: var(--e3); padding: 5px;
                max-height: calc(100vh - 150px);
                max-height: calc(100dvh - 150px); overflow-y: auto;
                overscroll-behavior: contain; }
.lib-doc { display: flex; align-items: center; gap: 8px; padding: 7px 10px;
           border-radius: 9px; cursor: pointer; font-size: 12.5px;
           color: var(--ink); }
.lib-doc:hover { background: var(--hover); }
.lib-doc input { accent-color: var(--ok); margin: 0; flex: none; }
.lib-doc.all { font-weight: 600; }
/* The badge sits between the checkbox and the name and must not shrink; the
   name takes what is left. `.filename` owns the clipping (app.css). */
.lib-doc .filebadge { width: 15px; height: 18px; flex: none; }
.lib-doc .filebadge b { font-size: 5.5px; padding-bottom: 2px; }
.lib-doc .filebadge::after { border-width: 0 0 5px 5px; }
.lib-doc-name { flex: 1; min-width: 0; }
.lib-scope { font-family: var(--font-mono); font-size: 9.5px;
             letter-spacing: .05em; text-transform: uppercase;
             color: var(--faint); flex: none; }
.lib-empty { padding: 4px 9px 2px; font-size: 12px; color: var(--graphite); }
.lib-empty-link { display: block; padding: 2px 9px 8px; font-size: 12px;
                  color: var(--ok-ink); text-decoration: none; font-weight: 600; }
.lib-empty-link:hover { text-decoration: underline; }

/* Grounding's empty case under an answer ("nothing in your documents matched").
   Successful grounding renders no excerpt block; see addAssistantTurn. */
.sources { margin-top: 12px; }
.src-kicker { font-family: var(--font-mono); font-size: 10px;
              letter-spacing: .07em; text-transform: uppercase;
              color: var(--faint); }

/* The retry chips: this turn was masked, and masking was not what you wanted.
   Sits quiet under the answer, because most turns that mask are correct --
   and it FOLDS (§101.5), because "quiet" was measured at nine buttons under a
   one-line reply. Closed, the whole thing is the kicker line it always had,
   plus how many kinds of value are behind it. */
.waive { margin-top: 12px; }
.waive > summary { display: flex; align-items: center; gap: 6px;
         width: fit-content; cursor: pointer; list-style: none;
         padding: 2px 0; }
.waive > summary::-webkit-details-marker { display: none; }
/* The disclosure triangle, drawn rather than borrowed: the native marker sits
   on the text baseline of a 10px uppercase kicker and reads as a stray dot. */
.waive > summary::before { content: "›"; font-family: var(--font-mono);
         font-size: 12px; line-height: 1; color: var(--faint);
         transition: transform .12s ease; }
.waive[open] > summary::before { transform: rotate(90deg); }
.waive > summary:hover { color: var(--ink-2); }
.waive > summary:hover::before { color: var(--ink-2); }
.waive > summary:focus-visible { outline: 2px solid var(--accent);
         outline-offset: 2px; border-radius: 4px; }
.waive-count { font-family: var(--font-mono); font-size: 10px;
         color: var(--faint); background: var(--hover);
         border-radius: 999px; padding: 0 5px; letter-spacing: 0; }
.waive-chips { display: flex; align-items: center; flex-wrap: wrap;
         gap: 8px; margin-top: 8px; }
.waive-btn { border-color: var(--warn); color: var(--warn); }
.waive-btn:hover { background: var(--warn-tint); color: var(--warn); }

/* Web sources ARE listed under the answer, unlike library ones: the reader owns
   the documents in their library and can go and check them, but a web page is a
   stranger's claim and the link is what makes the answer checkable at all. */
.sources.web .src-row { display: flex; align-items: baseline; gap: 8px;
                        margin-top: 5px; font-size: 12.5px; }
.sources.web .src-link { color: var(--ink-2); text-decoration: none;
                         border-bottom: 1px solid var(--border);
                         overflow: hidden; text-overflow: ellipsis;
                         white-space: nowrap; }
.sources.web .src-link:hover { color: var(--ink); border-bottom-color: var(--ink-2); }
.sources.web .src-date { flex: none; font-style: normal; font-size: 11px;
                         color: var(--faint); }

/* Drive files ARE listed under the answer, for the middle reason between those
   two: the reader owns these files, but they did not choose which of them was
   read, so the names are what make the answer checkable. Names only, never
   links: the browser never talks to Google, and nothing was copied here. The
   mark leads the kicker once, for the group. */
.sources.drive .src-kicker { display: flex; align-items: center; gap: 6px; }
.sources.drive .src-mark { display: grid; place-items: center; width: 12px;
                           height: 12px; flex: none; }
.sources.drive .src-mark svg { display: block; width: 12px; height: 11px; }
/* The brain's own `.src-row` is a BUTTON (padding, pointer, hover ground) and
   it reaches these through the one stylesheet the console ships, so the parts
   that would make a filename look clickable are put back. Nothing here is. */
.sources.drive .src-row { display: flex; align-items: baseline; gap: 8px;
                          margin-top: 5px; font-size: 12.5px; padding: 0;
                          cursor: default; }
.sources.drive .src-row:hover { background: none; }
/* `.src-file` and not `.src-name`: documents.css already owns that name for the
   brain's connected-source rows, and the console ships ONE stylesheet. */
.sources.drive .src-file { color: var(--ink-2); overflow: hidden;
                           text-overflow: ellipsis; white-space: nowrap; }

/* An explanation the reader is MEANT to read, in the slot a proposal card would
   have filled. Deliberately NOT .src-kicker: that is a 10px uppercase mono
   label built for four words, and three sentences set in it arrive as two lines
   of faint small caps, which is how the one paragraph that had to be read
   shipped unreadable. This is the reply's own body register, one step quieter. */
.turn-note { margin-top: 12px; padding: 12px 14px; border-radius: 12px;
             border: 1px solid var(--hair); background: var(--paper);
             font-size: 13.5px; line-height: 1.6; color: var(--ink-2); }
.turn-note p { margin: 0; }
.turn-note p + p { margin-top: 8px; }
.turn-note .mini-btn { margin-top: 11px; }

/* Proposed tracked changes under a reply: struck original, proposed wording,
   ticked by default. Nothing is applied until the download is asked for, and
   the stored document is never changed. */
/* The file Cedreon just wrote (§85). One row, because that is the whole of
   what it has to say: what it is called, what kind of file it is, and the two
   things to do with it. Sized and bordered like the proposal card next to it,
   so a turn that produced a document and a turn that proposed a change read
   as the same kind of event. */
.made-card { margin-top: 12px; display: flex; align-items: center; gap: 9px;
             padding: 10px 12px; border: 1px solid var(--hair);
             border-radius: 11px; background: var(--surface);
             box-shadow: var(--e1); }
.made-card .filebadge { flex: none; }
.made-name { font-weight: 600; font-size: 13px; min-width: 0;
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.made-kind { font-size: 11.5px; color: var(--faint); white-space: nowrap; }
.made-card .spacer { flex: 1; }
/* The download is an anchor, so it wears the button's clothes rather than
   the browser's: a styled span becoming an untagged link is the trap
   `button.turn-vaulted` already records from the other direction. */
a.mini-btn#made-download { text-decoration: none; display: inline-flex;
             align-items: center; }
@media (max-width: 560px) {
  .made-card { flex-wrap: wrap; }
  .made-card .spacer { flex-basis: 100%; }
}

.rl-card { margin-top: 12px; border: 1px solid var(--hair);
           border-radius: 13px; background: var(--surface);
           box-shadow: var(--e1); overflow: hidden; }
.rl-head { display: flex; align-items: center; gap: 9px; padding: 10px 14px;
           border-bottom: 1px solid var(--hair); }
.rl-head b { font-size: 12.5px; font-weight: 600; }
.rl-count { font-family: var(--font-mono); font-size: 10px; color: var(--faint); }
/* Why a card restored from a saved conversation cannot hand back a file, or
   which copy it was proposed against. Prose register, deliberately not the mono
   kicker next to it: this one exists to be read. */
.rl-note { padding: 10px 14px; border-bottom: 1px solid var(--hair);
           background: var(--paper); font-size: 13px; line-height: 1.55;
           color: var(--ink-2); }
/* The same prose register for a heavier fact: one of these changes lost wording
   when the conversation was saved, so it has no tick box and no file. */
.rl-loss { border-left: 3px solid var(--bad); }
.rl-partial { font-family: var(--font-mono); font-size: 10.5px;
              text-transform: uppercase; letter-spacing: .05em;
              color: var(--bad); }
.rl-edit { padding: 11px 14px; border-bottom: 1px solid var(--hair); }
.rl-edit label { display: flex; align-items: center; gap: 8px; margin: 0 0 4px;
                 font-size: 12px; cursor: pointer; }
.rl-edit input[type="checkbox"] { accent-color: var(--accent); }
.rl-kind { font-family: var(--font-mono); font-size: 10.5px;
           text-transform: uppercase; letter-spacing: .05em; color: var(--accent); }
.rl-anchor { font-family: var(--font-mono); font-size: 10.5px; color: var(--faint); }
.rl-old { color: var(--bad); text-decoration: line-through; white-space: pre-wrap;
          font-size: 12.5px; margin-top: 2px; padding-left: 24px; }
.rl-new { color: var(--ok-ink); white-space: pre-wrap; font-size: 12.5px;
          margin-top: 2px; padding-left: 24px; }
.rl-actions { display: flex; align-items: center; gap: 8px; padding: 10px 14px; }
.rl-actions .spacer { flex: 1; }
.rl-flash { margin: 0 14px 10px; font-size: 12.5px; color: var(--bad); }

/* The live screener's count, and the "always hide this" offer beside it. Both
   sit at the right end of the composer row where the send button is: this is
   the state of what you are about to send.

   THEY WEAR THE PROMPT CARD'S FACE, NOT THE PROTECTION GREEN. They used to be
   solid `--ok-tint`, and two filled green pills beside a green "Vault active"
   chip and a green "Company brain" pill made the composer read as a warning
   strip -- the loudest thing on a screen whose subject is the sentence being
   written. The offer is an OFFER, so it is drawn as one: the same surface,
   hairline border, radius and lift as `.prompt-card` above, with the icon in
   its own tinted square at the leading edge. `deny` is the one state that
   keeps its colour, because a draft the policy would refuse is the one thing
   here that must not read as ordinary. */
.count-pill { display: inline-flex; align-items: center; gap: 8px; height: 30px;
              padding: 0 12px 0 7px; border: 1px solid var(--border);
              border-radius: 20px;
              background: var(--surface); color: var(--ink-2);
              font-family: var(--font-sans); font-size: 12.5px; font-weight: 500;
              cursor: pointer; white-space: nowrap; box-shadow: var(--e1);
              transition: border-color .14s ease, color .14s ease,
                          transform .14s ease; }
.count-pill:hover { border-color: var(--border-2); color: var(--ink);
                    transform: translateY(-1px); }
.count-pill:focus-visible { outline: 2px solid var(--accent);
                            outline-offset: 2px; }
/* The icon square, sized down from `.card-icon` for a 30px row the way the
   file badge is: a pill is not as tall as the cards it borrows its face
   from. */
.count-pill .card-icon { width: 20px; height: 20px; flex: 0 0 20px;
                         border-radius: 6px; }
.count-pill .card-icon svg { width: 13px; height: 13px; }
.count-pill.deny { border-color: var(--bad); background: var(--bad-tint);
                   color: var(--bad); }
.count-pill.deny .card-icon { background: var(--bad-tint); color: var(--bad); }
.send-btn { width: 34px; height: 34px; flex: 0 0 auto; display: grid;
            place-items: center; border: 0; border-radius: 50%;
            background: var(--accent); color: var(--on-accent); cursor: pointer;
            margin-left: 5px;
            transition: background .14s ease, transform .14s ease,
                        opacity .14s ease; }
.send-btn:hover { background: var(--accent-strong); transform: translateY(-1px); }
.send-btn:disabled { opacity: .32; cursor: default; transform: none;
                     background: var(--accent); }
/* The slot under the composer. It used to hold the free-usage bar; since the
   owner cut that (2026-08) it carries the one-line disclaimer instead, the
   same quiet register every chat product prints where people type. */
/* A SENTENCE, so it is set in the sentence face. It was mono at 10px, which
   is the treatment this console reserves for placeholders, detector ids and
   sample values -- technical strings a reader is meant to compare character
   by character. "Cedreon can make mistakes" is none of those, and setting it
   as though it were is what made three unrelated type treatments meet under
   one composer. */
.composer-note { text-align: center; font-family: var(--font-sans);
                 font-size: 11px; color: var(--faint); margin-top: 9px;
                 min-height: 15px; }
#chat-app[data-empty] .composer-note { min-height: 0; }
.composer-usage { display: inline-flex; align-items: center; gap: 8px; }
.composer-usage .meter { width: 90px; height: 4px; }

/* --------------------------------------------------------------- side pane --- */
#side-pane { width: var(--pane-w, 384px); flex: 0 0 var(--pane-w, 384px);
             display: flex; position: relative;
             flex-direction: column; min-height: 0; min-width: 0;
             background: var(--side); border-left: 1px solid var(--hair); }
.pane-resizer { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px;
                cursor: col-resize; z-index: 5; }
.pane-resizer:hover, .pane-resizer.active { background: var(--accent-tint); }
#side-pane[hidden] { display: none; }
/* A document is open beside the chat (§72): the pane is a real half of the
   window, and a width the reader chose for themselves still wins. */
#side-pane.with-document { width: max(var(--pane-w, 384px), min(48vw, 760px));
                           flex: 0 0 max(var(--pane-w, 384px), min(48vw, 760px)); }
.pane-top { display: flex; align-items: center; gap: 4px;
            padding: 12px 14px; }
.pane-tabs { display: flex; gap: 4px; flex: 1; }
.pane-tabs [role="tab"] { display: inline-flex; align-items: center; gap: 6px;
                          height: 32px; padding: 0 12px; border: 0;
                          border-radius: 8px; background: transparent;
                          color: var(--graphite); font-size: 13px;
                          font-weight: 500; cursor: pointer;
                          font-family: var(--font-sans); }
.pane-tabs [role="tab"]:hover { background: var(--hover); color: var(--ink); }
.pane-tabs [role="tab"][aria-selected="true"] { background: var(--surface);
                          color: var(--ink); box-shadow: var(--e1); }
.tab-count { font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;
             background: var(--ok-tint); color: var(--ok-ink);
             border-radius: 999px; padding: 0 6px; min-width: 16px;
             text-align: center; }
.pane-body { flex: 1; overflow-y: auto; min-height: 0; padding: 0 14px 14px;
             scrollbar-width: thin; }

/* ONE document presentation, for every surface that draws a document beside
   the chat: the editing session and the pre-send vault review both render
   DocumentShell, so these rules are scoped to .document-shell rather than to
   either pane. Drawn by the same reader classes the documents page ships
   (.doc-page/.dp travel unscoped in documents.css); what that sheet scopes to
   #docs-app is restated here for the pane. The bar stays put while the
   document scrolls under it, so what the reader can DO is never further away
   than the thing they are reading. */
.changes-head { display: flex; align-items: center; gap: 8px;
               padding: 4px 2px 10px; }
.changes-name { font-weight: 600; font-size: 13px; min-width: 0;
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.document-shell p.rl-note { padding: 9px 12px; border: 1px solid var(--hair);
                         border-radius: 11px; background: var(--surface);
                         margin: 0 0 10px; }
.document-shell .doc-masthead { position: sticky; top: 0; z-index: 4;
                          background: var(--side); padding-top: 2px;
                          box-shadow: 0 6px 8px -8px rgba(0, 0, 0, .35); }
/* The hint is a sentence, not a field. In the basket's box it read as an
   empty input the reader could click into and type the change. */
.document-shell p.rl-note#bound-hint { border: 0; background: none;
                          color: var(--ink-2); padding: 2px 2px 10px; }
.document-shell .rl-basket { position: static; z-index: 3; display: flex;
                          align-items: center; flex-wrap: wrap; gap: 8px;
                          padding: 9px 12px; border: 1px solid var(--hair);
                          border-radius: 11px; background: var(--surface);
                          box-shadow: var(--e1); }
.document-shell .rl-basket .spacer { flex: 1; }
.document-shell .rl-basket .rl-note { padding: 0; border: 0; background: none;
                                   color: var(--faint); font-size: 11.5px; }
.document-shell .rl-basket .rl-flash { margin: 0; flex-basis: 100%; }
.document-shell .rl-basket .rl-done { margin: 0; flex-basis: 100%;
                                   font-size: 12px; color: var(--ok-ink); }
.document-shell .changes-note { color: var(--ink-2); font-size: 12.5px;
                            padding: 12px 2px; }
.document-shell .changes-caveat { color: var(--faint); font-size: 11px;
                              line-height: 1.5; margin: 6px 2px 0; }
.document-shell details.changes-caveat > summary { cursor: pointer;
                              list-style: none; padding: 2px 0;
                              color: var(--ink-2); font-size: 11.5px; }
.document-shell details.changes-caveat > summary::-webkit-details-marker {
                              display: none; }
.document-shell details.changes-caveat > summary::before { content: "\203a ";
                              display: inline-block; margin-right: 5px;
                              transition: transform .12s ease; }
.document-shell details.changes-caveat[open] > summary::before {
                              transform: rotate(90deg); }
.document-shell details.changes-caveat > p { margin: 4px 0 0; }
/* A change scrolled to must clear the masthead that is pinned over it. */
.document-shell [data-para], .document-shell [data-anchor] {
                              scroll-margin-top: 92px; }
.document-shell .changes-head .spacer { flex: 1; }
/* The read-only line stands where the basket would: same box, so the pane
   does not shift when a Word file is swapped for a PDF, and the sentence
   answers the question the missing basket raises. */
.document-shell #bound-readonly { color: var(--faint); font-size: 11.5px; }
/* What the download is, said quietly under the document. It explains a
   button; it is not a warning, so it does not wear the notice box. */
.document-shell p#bound-download-note { padding: 0 2px; border: 0;
                                        background: none; color: var(--faint);
                                        font-size: 11.5px; margin: 10px 0 0; }
.document-shell #bound-close-confirm { border-color: var(--accent); }
/* The PDF is drawn by the browser from the original bytes: a frame, filling
   what is left of the pane under the head. */
.document-shell .pdf-stage { flex: 1; min-height: 320px; display: flex;
                          margin-top: 10px; }
.document-shell .pdf-stage iframe { flex: 1; width: 100%; min-height: 320px;
                                 border: 1px solid var(--hair);
                                 border-radius: 11px; background: var(--surface); }
.document-shell .rl-inline-keep { margin: 2px 0 12px; display: flex;
                                  align-items: center; gap: 8px; }
.document-shell .rl-inline-keep .turn-chip.kept { color: var(--ok-ink);
                                               border-color: var(--ok-ink); }
/* Which side of the decision this change is on, in a word, beside the press
   that moves it to the other side. */
.rl-state { font-family: var(--font-mono); font-size: 10px;
            letter-spacing: .04em; text-transform: uppercase;
            padding: 2px 7px; border-radius: 999px; }
.rl-state.kept { background: var(--ok-tint); color: var(--ok-ink); }
.rl-state.removed { background: var(--canvas-2); color: var(--faint); }
/* Removed, not gone: the proposal keeps its place so it can be reconsidered,
   and reads as withdrawn rather than as part of the document. */
.dp.ins.out { opacity: .45; text-decoration: line-through;
              text-decoration-color: var(--faint); }
.document-shell .rl-inline-keep.out { opacity: .8; }
/* The chip a selection raises: floats at the selection (viewport coordinates,
   re-anchored on scroll) and pins the passage on the composer. */
.suggest-chip { position: fixed; z-index: 80; background: var(--surface);
                box-shadow: var(--e2, var(--e1)); white-space: nowrap;
                max-width: min(340px, 80vw); }
/* The exact selected value, shown back inside the chip so "Vault this" says
   WHAT it would hide. Bounded, with the whole of it on the title. */
.suggest-chip-value { margin-left: 7px; padding-left: 7px;
                      border-left: 1px solid var(--hair); color: var(--ink-2);
                      font-family: var(--font-mono); font-size: 10.5px;
                      max-width: 190px; overflow: hidden;
                      text-overflow: ellipsis; vertical-align: bottom;
                      display: inline-block; }

/* Vault tab */
.vault-intro { display: flex; gap: 11px; padding: 13px 14px;
               background: var(--surface); border: 1px solid var(--hair);
               border-radius: 12px; }
.vault-icon { width: 28px; height: 28px; flex: 0 0 auto; border-radius: 8px;
              background: var(--ok-tint); color: var(--ok);
              display: grid; place-items: center; }
.vault-intro-title { font-family: var(--font-display); font-weight: 650;
                     font-size: 15px; }
.vault-intro-sub { font-size: 12.5px; color: var(--ink-2); line-height: 1.5;
                   margin-top: 2px; }
.vault-live-head { display: flex; align-items: center;
                   justify-content: space-between; margin: 16px 4px 9px; }
.count-badge { font-family: var(--font-mono); font-size: 11px; font-weight: 600;
               padding: 2px 9px; border-radius: 999px;
               background: var(--hover); color: var(--graphite); }
.count-badge.live { background: var(--ok-tint); color: var(--ok-ink); }
#vault-list { display: flex; flex-direction: column; gap: 8px; }
.vault-row { display: flex; align-items: center; gap: 11px; padding: 11px 13px;
             background: var(--surface); border: 1px solid var(--hair);
             border-radius: 11px; transition: border-color .14s ease; }
.vault-row:hover { border-color: var(--ok-line); }
.vault-row .vault-icon { width: 26px; height: 26px; }
.vault-row-main { flex: 1; min-width: 0; }
.vault-row-top { display: flex; align-items: center; gap: 7px; }
.vault-type { font-family: var(--font-mono); font-size: 9.5px; font-weight: 500;
              letter-spacing: .1em; text-transform: uppercase;
              color: var(--ok-ink); }
.vault-masked { font-size: 13px; color: var(--ink); white-space: nowrap;
                overflow: hidden; text-overflow: ellipsis;
                border-bottom: 1.5px solid var(--ok-line); }
.vault-row-sub { display: flex; align-items: center; gap: 6px; margin-top: 6px;
                 font-family: var(--font-mono); font-size: 11px;
                 color: var(--graphite); }
.vault-empty { display: flex; flex-direction: column; align-items: center;
               text-align: center; gap: 8px; padding: 18px 16px;
               background: var(--surface); border: 1px dashed var(--border-2);
               border-radius: 12px; color: var(--faint); }
.vault-empty b { font-size: 13px; color: var(--graphite); }
.vault-deny { display: flex; gap: 9px; margin-top: 10px; padding: 11px 13px;
              background: var(--bad-tint); border: 1px solid var(--bad);
              border-radius: 11px; font-size: 12px; color: var(--bad);
              line-height: 1.5; }
/* Two disclosures at the foot of the panel, same affordance: what the model
   actually received on the last turn, and the full list of what gets screened.
   The panel opens on its own, so its resting state has to stay quiet. */
.vault-screened, .vault-saw { margin: 14px 0 4px;
                              border-top: 1px solid var(--hair);
                              padding-top: 6px; }
.vault-screened summary, .vault-saw summary {
                  list-style: none; cursor: pointer; user-select: none;
                  display: flex; align-items: center; gap: 7px;
                  padding: 9px 6px; border-radius: 8px;
                  font-size: 12.5px; color: var(--ink-2); }
.vault-screened summary::-webkit-details-marker,
.vault-saw summary::-webkit-details-marker { display: none; }
.vault-screened summary:hover, .vault-saw summary:hover {
                  background: var(--hover); color: var(--ink); }
.vault-screened summary::before, .vault-saw summary::before {
                  content: "›"; display: inline-block; margin-left: auto;
                  order: 9; color: var(--faint);
                  transition: transform .18s ease; }
.vault-screened[open] summary::before,
.vault-saw[open] summary::before { transform: rotate(90deg); }
.screened-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 6px 6px; }
.screened-chip { font-family: var(--font-mono); font-size: 10.5px;
                 color: var(--ink-2); background: var(--surface);
                 border: 1px solid var(--hair); border-radius: 999px;
                 padding: 3px 10px; }
.screened-chip.ner { color: var(--layer2); border-color: var(--layer2); }
/* WHAT THE MODEL SAW: the last turn's masked text, verbatim, with every
   placeholder marked. Server-reported (masked_input) and rendered as text
   nodes; the client never computes it. */
.saw-body { margin: 2px 6px 4px; padding: 12px 13px; background: var(--surface);
            border: 1px solid var(--hair); border-radius: 10px;
            font-family: var(--font-mono); font-size: 11.5px; line-height: 1.72;
            color: var(--ink-2); white-space: pre-wrap;
            overflow-wrap: break-word;
            /* The conversation receipt grows with the chat; capped so a long
               history scrolls inside its card instead of swallowing the
               panel. Opens scrolled to the newest turn (SidePanel). */
            max-height: 340px; overflow: auto; }
.saw-body b { color: var(--ok-ink); font-weight: 500;
              background: var(--ok-tint); border-radius: 3px; padding: 0 2px; }
.saw-note { font-size: 11px; color: var(--ink-2); margin: 9px 6px 0;
            line-height: 1.5; }
.vault-note-card { display: flex; gap: 9px; margin-top: 16px; padding: 11px 13px;
                   background: var(--ok-tint); border: 1px solid var(--ok-line);
                   border-radius: 11px; font-size: 12px; color: var(--ok-ink);
                   line-height: 1.5; }

/* Document tab */
.doc-card { background: var(--surface); border: 1px solid var(--hair);
            border-radius: 13px; display: flex; flex-direction: column;
            min-height: 0; height: 100%; overflow: hidden; }
#doc-view { display: flex; flex-direction: column; min-height: 0; }
#doc-view[hidden] { display: none; }
.doc-head { display: flex; align-items: center; gap: 8px;
            padding: 11px 13px; border-bottom: 1px solid var(--hair); }
.doc-title { font-family: var(--font-display); font-weight: 650; font-size: 15px;
             flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
             white-space: nowrap; }
.doc-tabs { display: flex; gap: 2px; border-radius: 8px; padding: 2px;
            background: var(--paper); }
.doc-tabs button { border: 0; background: transparent; font-size: 11.5px;
                   font-weight: 600; padding: 3px 10px; border-radius: 6px;
                   color: var(--graphite); cursor: pointer;
                   font-family: var(--font-sans); }
.doc-tabs button.active { background: var(--surface); color: var(--ink);
                          box-shadow: var(--e1); }
#doc-body { flex: 1; overflow: auto; min-height: 0; padding: 13px 15px;
            font-size: 13.5px; }
#doc-body pre.src { margin: 0; font-family: var(--font-mono); font-size: 12px;
                    line-height: 1.6; white-space: pre-wrap;
                    word-break: break-word; }
#doc-body textarea.doc-edit { width: 100%; height: 100%; min-height: 260px;
                    border: 0; padding: 0; resize: none; background: none;
                    font-family: var(--font-display); font-size: 13.5px;
                    line-height: 1.7; color: var(--ink-2); box-shadow: none; }
.doc-foot { display: flex; align-items: center; gap: 8px; padding: 9px 12px;
            border-top: 1px solid var(--hair); background: var(--paper); }
.doc-meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--faint); }

/* --------------------------------------------------------------- auth card --- */
/* Signing in and signing up happen IN the conversation flow: #auth-flow is a
   block inside #msgs that chat.js appends like a turn, never a fixed overlay,
   so nothing is blurred and the transcript stays readable while the form is
   open. The card itself is shared with the budget wall below, which keeps its
   own fixed .wall-overlay (a different audience: signed-in capped accounts). */
.auth-flow { padding: 4px 0 10px; }
.auth-flow[hidden] { display: none; }
.auth-flow .auth-card { margin: 0 auto; max-width: 100%; max-height: none;
                        overflow-y: visible; border: 1px solid var(--border);
                        box-shadow: var(--e1); }
.auth-card { position: relative; width: 420px; max-width: 92vw;
             max-height: 92vh; overflow-y: auto;
             background: var(--surface); border-radius: 18px;
             box-shadow: var(--e3); padding: 30px 28px 20px; }
.auth-close { position: absolute; top: 12px; left: 12px; width: 32px; height: 32px;
              display: grid; place-items: center; border: 0; border-radius: 9px;
              background: transparent; color: var(--faint); cursor: pointer; }
.auth-close:hover { background: var(--hover); color: var(--ink-2); }
.auth-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.auth-brand { display: flex; flex-direction: column; align-items: center;
              gap: 9px; margin-bottom: 16px; text-align: center; }
.auth-mark { width: 46px; height: 46px; border-radius: 13px; background: var(--accent);
             color: var(--on-accent); display: grid; place-items: center; }
.auth-name { font-family: var(--font-display); font-weight: 650; font-size: 23px;
             line-height: 1; }
.auth-tag { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .14em;
            text-transform: uppercase; color: var(--faint); }
.auth-note { margin: 0 0 14px; padding: 9px 12px; border-radius: 9px;
             background: var(--warn-tint); color: var(--warn); font-size: 12.5px;
             font-weight: 600; text-align: center; }
.auth-foot { margin: 16px 0 0; text-align: center; font-family: var(--font-mono);
             font-size: 10px; letter-spacing: .05em; color: var(--faint); }
/* "Use a different account" signs out, and signing out is a POST (§10.3):
   the foot is the form, and its button reads as the link it replaced. */
.auth-foot button { padding: 0; border: 0; background: none; font: inherit;
                    letter-spacing: inherit; color: var(--accent);
                    text-decoration: underline; cursor: pointer; }
/* The free-trial wall reuses the auth card, but is a distinct overlay (never
   the sign-in surface) so signed-in pages carry no auth card at all. */
.wall-overlay { position: fixed; inset: 0; z-index: 90; display: grid;
                place-items: center; background: var(--scrim);
                backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px); }
.wall-actions { display: flex; flex-direction: column; gap: 8px; }
.wall-actions .btn { width: 100%; padding: 11px; }
.auth-tabs { display: flex; gap: 4px; background: var(--paper);
             border-radius: 10px; padding: 3px; margin-bottom: 16px; }
.auth-tabs label { flex: 1; text-align: center; padding: 8px 6px;
                   border-radius: 8px; font-size: 13px; font-weight: 600;
                   color: var(--graphite); cursor: pointer; }
#auth-in:checked ~ .auth-tabs label[for="auth-in"],
#auth-up:checked ~ .auth-tabs label[for="auth-up"] {
  background: var(--surface); color: var(--ink); box-shadow: var(--e1); }
.auth-form { display: none; }
#auth-in:checked ~ .auth-form-in { display: block; }
#auth-up:checked ~ .auth-form-up { display: block; }
.auth-form label { margin-top: 12px; }
/* The acceptance tick under the password. One ROW, not a stacked block: the
   box and its single sentence sit on one line and the sentence wraps under
   itself, so the two document links read as one acceptance rather than as a
   checklist. `align-items: flex-start` keeps the box on the FIRST line of a
   wrapped sentence instead of floating to its vertical middle, and the box
   is told not to shrink because a flex item with a long neighbour otherwise
   gets squeezed to nothing on a narrow screen. It overrides the label rule
   above rather than sitting beside it: these labels are stacked field
   captions and this one is not a caption at all. */
.auth-form label.auth-consent {
  display: flex; align-items: flex-start; gap: 9px; margin-top: 16px;
  font-size: 12.5px; line-height: 1.5; color: var(--graphite);
  font-weight: 400; cursor: pointer;
}
.auth-form label.auth-consent input[type="checkbox"] {
  flex: 0 0 auto; width: 14px; height: 14px; margin: 2px 0 0;
}
.auth-form .btn { width: 100%; margin-top: 18px; padding: 11px; }
.auth-error { margin: 0 0 14px; padding: 9px 12px; border-radius: 9px;
              background: var(--bad-tint); color: var(--bad); font-size: 12.5px;
              font-weight: 600; text-align: center; }
/* The check-inbox state's resend button: NOT .auth-form (that class is the
   radio-tab switcher and defaults to display:none). */
.auth-resend .btn { width: 100%; margin-top: 6px; padding: 11px; }
/* Google sign-in: a plain same-origin anchor under whichever tab is open,
   with the divider line naming the alternative. */
.auth-or { display: flex; align-items: center; gap: 10px; margin: 14px 0 0;
           color: var(--faint); font-family: var(--font-mono);
           font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px;
                                    background: var(--border); }
.auth-google { display: flex; align-items: center; justify-content: center;
               gap: 9px; width: 100%; margin-top: 12px; padding: 10px; }

/* THE AUTH CARD WEARS THE APP'S DRESS, everywhere it is shown (`auth-app`:
   the standalone /console/login and /console/signup pages, the signed-out
   chat shell, the handoff and the ready page; §112). One look for the
   sign-in whether it was reached from the landing page or from the desktop
   app, so the person who just left the app's door recognises the card they
   land on: the harness's 12px-radius layer-2 card on a hairline border,
   ink-on-white primary button, outlined secondary, and no coloured notice
   box -- the app sets a notice as quiet secondary text. Tokens are the
   site's own where one exists, so the dark theme follows by itself. A page
   the app opened (`from-app`) additionally drops the site header.
   The standalone pages also carry `app-dress` (app-dress.css, §137), which
   re-points those tokens at the release's palette and gives the wordmark the
   display face the app took from the website in 0.2.14. The signed-out chat
   shell does not: it keeps this skin as written until it is replaced. */
.auth-page.auth-app { min-height: calc(100vh - 96px); align-items: center;
                      padding: 24px 16px; }
/* The sign-out card (logout.html, §10.3) centres like app_ready's, from the
   stylesheet rather than an inline <style>, so it renders the same under a
   strict Content-Security-Policy. */
.auth-page.logout-page { display: flex; justify-content: center; }
.auth-app .auth-card { width: 440px; border-radius: 12px;
                                 border: 1px solid var(--border);
                                 box-shadow: 0 1px 2px rgba(0,0,0,.04);
                                 padding: 28px 32px 24px; }
.auth-app .auth-brand { gap: 10px; margin-bottom: 14px; }
.auth-app .auth-mark { width: 40px; height: 40px; border-radius: 50%;
                                 background: transparent; color: var(--ink);
                                 border: 5px solid currentColor; }
.auth-app .auth-mark svg { display: none; }
.auth-app .auth-name { font-family: inherit; font-size: 20px;
                                 font-weight: 500; line-height: 28px; }
.auth-app .auth-tag { font-family: inherit; font-size: 13px;
                                letter-spacing: 0; text-transform: none;
                                color: var(--ink-2); line-height: 1.5; }
.auth-app .auth-note { background: transparent; color: var(--ink-2);
                                 font-weight: 400; padding: 0 4px;
                                 font-size: 13px; line-height: 1.5; }
.auth-app .auth-tabs { background: transparent; border: 1px solid var(--border);
                                 border-radius: 10px; }
.auth-app .auth-tabs label { font-weight: 500; color: var(--ink-2); }
.auth-app #auth-in:checked ~ .auth-tabs label[for="auth-in"],
.auth-app #auth-up:checked ~ .auth-tabs label[for="auth-up"] {
  background: var(--ink); color: var(--surface); box-shadow: none; }
.auth-app .auth-form label:not(.auth-consent) {
  font-size: 12.5px; font-weight: 500; color: var(--ink-2); margin-top: 10px; }
/* The acceptance row keeps its own voice (chat.css above): a sentence with a
   box, not a field caption. Restated here because the skin's label rule
   would otherwise outrank it by specificity. */
.auth-app .auth-form label.auth-consent {
  font-size: 12.5px; font-weight: 400; color: var(--graphite); margin-top: 16px; }
.auth-app .auth-form input:not([type="checkbox"]) {
  border-radius: 8px; border: 1px solid var(--border); background: var(--paper);
  padding: 8px 11px; font-size: 13.5px; }
.auth-app .auth-form input:focus-visible { outline: 2px solid var(--ink);
                                                     outline-offset: 1px; border-color: var(--border); box-shadow: none; }
.auth-app .btn.primary { background: var(--ink); color: var(--surface);
                                   border-color: transparent; border-radius: 8px;
                                   font-weight: 500; }
.auth-app .btn.ghost { border: 1px solid var(--border); border-radius: 8px;
                                 background: var(--paper); color: var(--ink);
                                 font-weight: 500; }
.auth-app .auth-or { font-family: inherit; letter-spacing: 0;
                               text-transform: none; font-size: 12px; }
.auth-app .auth-foot { font-family: inherit; font-size: 12.5px;
                                 letter-spacing: 0; color: var(--ink-2); }
.auth-app .auth-error { border-radius: 8px; font-weight: 500; }

/* -------------------------------------------------------------- responsive --- */
/* Deliberately NO drawer and NO pane-priority rule here. The rail is the
   same static column at every width; hiding it when the vault pane opened,
   or sliding it off screen under 900px, read as the interface jumping
   sideways. The manual toggle (body[data-rail-collapsed], app.css) is the
   one way it leaves, at any width. */
@media (max-width: 899px) {
  /* The document takes the screen on a phone, which is the only way it is
     readable - but it is the SAME conversation underneath, and the pane's own
     ✕ (and the document's Close) put the chat back. Nothing here starts a
     second session or navigates away. */
  #side-pane, #side-pane.with-document { position: fixed; inset: 0; z-index: 70;
               width: auto; flex: 0 0 auto; }
  .pane-resizer { display: none; }
  .vault-pill span + span { display: none; }
  #chat-heading { max-width: none; }
  .turn-model .cardinfo { left: auto; right: 0; }
  #msgs, #composer { padding-left: 16px; padding-right: 16px; }
  .turn.you .turn-card { max-width: 92%; }
  /* Touch targets: 32px is comfortable for a cursor, not for a thumb. The
     header's ⓘ used to be listed here; the only `.model-card summary` left is
     the one wrapped around an answer's model name, which is a line of text
     and must not be squared off to 40px. */
  #sidebar-toggle { width: 40px; height: 40px; }
  .send-btn { width: 40px; height: 40px; flex-basis: 40px; }
}
/* The rail is still never a drawer: nothing here hides it as the window
   changes size, and the toggle remains the only thing that moves it. This is
   the ONE case where it is not rendered at all, and it is not a width rule
   about the rail so much as about the toggle: signed out, chat.html renders
   no `[data-rail-toggle]` (the template gates it on `not signed_out`) and the
   page loads no chat script, so there is nothing on the page that could ever
   close it. On a 375px phone that left an unclosable 274px column -- 73% of
   the screen -- with the auth card crushed into the ~100px beside it, one word
   per line. Nothing is lost by dropping it: signed out the rail holds the
   brand and the two auth entries, and the auth card carries both already. */
@media (max-width: 720px) {
  body.signed-out #convo-pane { display: none; }
}
/* The composer's tool row does not wrap (the send button has to stay on the
   same line as the text), so on a phone the trigger drops its word and the
   sparkle carries it. */
@media (max-width: 620px) {
  #tools-label { display: none; }
  #tools-trigger { padding: 0 8px; }
}
/* ------------------------------------------- composer menus on a phone --- */
/* The three composer menus stop being anchored to the composer and become
   viewport-anchored sheets with their own scroll area.

   They were absolutely positioned inside #composer and capped against the
   WINDOW, which is a measurement the composer cannot make: `bottom: 100%`
   opens upward from wherever the box happens to sit, and on the first-run home
   the rule above it flips them downward from a composer at optical CENTRE with
   `max-height: min(420px, 44vh)`. On a 390x844 phone that is a 370px list
   hanging off a box halfway down the screen, and `body.app-shell` is
   `height: 100dvh; overflow: hidden` (app.css) -- so the bottom of the list was
   not merely off screen, it was CLIPPED, with no page scroll and no menu scroll
   able to reach it. The models below the fold could not be selected at all.

   `position: fixed` is what makes the cap honest: the sheet is measured against
   the same viewport it is capped against, so `max-height` is the scroll area
   rather than a guess about the composer's whereabouts. No ancestor creates a
   containing block (nothing on #composer, #chat-pane or #chat-app sets
   transform/filter/contain), so it escapes that overflow: hidden -- the same
   technique .convo-menu already uses for the same reason. It also drops out
   from under the empty-state rule's `top`/`max-height` by resetting both, so
   the first-run home and a running conversation get the same reachable list.

   z-index sits above the composer and the transcript and below the side pane
   (70) and the rail scrim (60), which are the two things that legitimately
   cover the whole window. */
@media (max-width: 620px) {
  #model-menu, #tools-menu, #library-menu, #effort-menu,
  #chat-app[data-empty] #model-menu,
  #chat-app[data-empty] #tools-menu,
  #chat-app[data-empty] #effort-menu,
  #chat-app[data-empty] #library-menu {
    position: fixed; left: 10px; right: 10px; width: auto; max-width: none;
    top: auto; bottom: 10px; z-index: 55;
    /* Bounded by the window on BOTH sides: never taller than the space there
       is (the 20px is the two gutters), and never more than two thirds of it,
       so the composer that opened the sheet stays visible behind it and the
       reader keeps their place. */
    max-height: min(66vh, calc(100vh - 20px));
    max-height: min(66dvh, calc(100dvh - 20px));
    overflow-y: auto; overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  /* The document picker opens from a row INSIDE the tools sheet, so left as an
     absolute box it is clipped by the sheet that now scrolls around it. Fixed
     like its parent and one step above it, it is a sheet in its own right. */
  #library-menu { z-index: 56; }
  /* A finger is not a cursor: the rows a thumb has to hit get the 44px the
     rest of the phone layout already gives its controls. */
  #model-menu [role="option"] { padding: 11px 12px; }
  #model-menu [role="option"] .opt-name { font-size: 14px; }
  /* The description may wrap to a second line here. The one-line clamp exists
     because a two-line row is what made the DESKTOP picker taller than the
     viewport; this sheet is capped against the viewport and scrolls, so the
     clamp buys nothing and costs the reader the sentence they are choosing on.
     Two lines at most, so a long description cannot push the list out again. */
  #model-menu .opt-desc { white-space: normal; overflow: hidden;
                          display: -webkit-box; -webkit-box-orient: vertical;
                          -webkit-line-clamp: 2; line-clamp: 2; }
}
/* A message from us to this account (messages.py): sits above the transcript
   until dismissed. Deliberately quiet: it is a note, not an alert.
   width/max-width rather than a plain max-width: the notice is a SIBLING of
   #msgs, so it has to reproduce that column's gutter itself or it bleeds to
   the screen edges on a phone. */
.notice { display: flex; align-items: flex-start; gap: 14px;
          width: calc(100% - 56px); max-width: var(--measure);
          margin: 14px auto 0; padding: 12px 14px;
          border: 1px solid var(--border); border-left: 3px solid var(--accent);
          border-radius: 12px; background: var(--surface); box-shadow: var(--e1); }
.notice-body { flex: 1; min-width: 0; }
.notice-from { display: block; font-size: 11px; font-weight: 600;
               color: var(--graphite); margin-bottom: 3px; }
.notice-body p { margin: 0; font-size: 13.5px; color: var(--ink);
                 white-space: pre-wrap; overflow-wrap: anywhere; }
.notice form { flex: none; }

/* ------------------------------------------------- pre-send vault review ---
   §52. A staged file says on its chip what Cedreon intends to hide in it, and
   Review opens the panel on the file itself. The marks reuse the composer's own
   visual language (a green underlay under a green rule) because they mean the
   same thing there and here: this will not reach the model. Manual additions
   carry one extra rule on the left, a suggestion is dashed and not yet real. */
.attach-chip .attach-vault { color: var(--ok-ink); font-size: 11px;
                             font-family: var(--font-mono);
                             white-space: nowrap; }
.attach-chip .attach-vault.pending { color: var(--graphite); }
.attach-chip .attach-review { border: 1px solid var(--border); background: none;
                              color: var(--ink-2); font: inherit;
                              font-size: 11px; padding: 1px 8px;
                              border-radius: 999px; cursor: pointer; }
.attach-chip .attach-review:hover { border-color: var(--ok-line);
                                    color: var(--ok-ink); }
/* The action that OPENS the file beside the chat. It sits next to the vault
   review and is drawn the same way, because both are inspections of the
   attachment rather than things that send it; only the accent differs, since
   this one is the door into the editing session. */
.attach-chip .attach-open { border: 1px solid var(--border); background: none;
                            color: var(--ink-2); font: inherit;
                            font-size: 11px; padding: 1px 8px;
                            border-radius: 999px; cursor: pointer;
                            white-space: nowrap; }
.attach-chip .attach-open:hover { border-color: var(--accent);
                                  color: var(--accent); }

/* Beside the send button: a STATE, never a switch. Privacy is always on, so
   there is nothing here to turn off and nothing here that opens a menu. */
#vault-stage { display: flex; align-items: center; gap: 7px;
               margin-bottom: 8px; color: var(--ok-ink); font-size: 12px; }
#vault-stage.pending { color: var(--graphite); }
#vault-stage svg { flex: none; }

/* The switcher sits IN the masthead now, under the filename and above the
   vault bar (DocumentShell fixes that order for every surface), so its room
   comes from the head's own bottom padding rather than from a top margin of
   its own. It used to be the first child of the card, which is how a surface
   that passed one drew its header 18px lower than a surface that did not. */
.document-shell .review-files { display: flex; flex-wrap: wrap; gap: 6px;
                             margin: 0 0 10px; }
.review-file { display: inline-flex; align-items: center; gap: 6px;
               max-width: 100%; padding: 4px 9px; border-radius: 999px;
               border: 1px solid var(--hair); background: var(--paper);
               color: var(--ink-2); font: inherit; font-size: 11.5px;
               cursor: pointer; }
.review-file.active { border-color: var(--ok-line); background: var(--ok-tint);
                      color: var(--ok-ink); }
.review-file .filebadge { width: 13px; height: 16px; flex: none; }
.review-file .filebadge b { font-size: 5px; padding-bottom: 2px; }
.review-file .filebadge::after { border-width: 0 0 5px 5px; }
/* Room to tell two of a matter's documents apart (§100.2). This was 130px of
   a 585px pane, which showed `AI Adoption in European Gov` and an ellipsis for
   both halves of a pair; `.filename` keeps each end, and the cap is now a
   share of the pane rather than a fixed number of pixels, so a wide panel
   spends its width and a narrow one still fits three tabs on a row. */
.review-file-name { max-width: min(300px, 42vw); }
.review-file-count { font-family: var(--font-mono); font-size: 10.5px;
                     color: inherit; opacity: .8; }

.review-note { margin: 6px 0 10px; color: var(--ink-2); font-size: 12px;
               line-height: 1.5; }
.review-tally { display: inline-flex; flex-wrap: wrap; gap: 12px;
                font-family: var(--font-mono); font-size: 11px;
                color: var(--graphite); }
.review-tally .suggested { color: var(--warn); }

/* The review's page is the SAME page the editing session draws (.doc-page),
   so it inherits the width, the centring and the room. What it adds is the
   typography of a paragraph, because its content is one run of extracted text
   rather than a block tree: matching .dp is what makes the two surfaces read
   as one document rather than as two views of a file. */
#review-doc, #docvault-doc { font-size: 13.5px; line-height: 1.7; color: var(--ink);
              white-space: pre-wrap; overflow-wrap: anywhere; }
/* A real highlight, not a band measured off the line box: on the document
   page the line height is a paragraph's, so the old gradient stop landed
   under the words and every protected value read as an underline.

   ONE RULE FOR BOTH SURFACES. The vault view of a document and the pre-send
   review of a staged one are the same document in the same shell, so a
   protected value has to look the same wherever the reader meets it -- and it
   did not: the vault view had its own copy of this declaration with no
   underline and a 3px radius, so the same .docx opened two ways marked its
   values two ways. The variants below stay scoped to the review, because
   `manual` and `suggested` are states only a pre-send review has. */
#review-doc .review-mark,
#docvault-doc .review-mark { color: inherit;
                           /* Mixed from the line colour rather than the flat
                              tint: a protected value has to be legible AS
                              protected at a glance, and --ok-tint over the
                              page reads as almost nothing. Theme-aware, so
                              the dark palette mixes its own. */
                           background: color-mix(in srgb, var(--ok-line) 34%,
                                                 transparent);
                           border-bottom: 1.5px solid var(--ok-line);
                           border-radius: 2px; padding: 0 1px;
                           -webkit-box-decoration-break: clone;
                           box-decoration-break: clone; }
#review-doc .review-mark.manual { border-left: 2px solid var(--ok-ink);
                                  padding-left: 1px; }
#review-doc .review-mark.suggested { background: none; color: inherit;
                                     border-bottom: 1.5px dashed var(--warn);
                                     box-shadow: none; }
.review-hint { margin: 8px 0 0; color: var(--faint); font-size: 11.5px; }

.review-suggest { margin-top: 18px; }
.review-ask { display: block; font-size: 12px; font-weight: 500;
              color: var(--ink-2); margin-bottom: 6px; }
.review-ask-row { display: flex; gap: 7px; }
.review-ask-row input { flex: 1; min-width: 0; height: 30px; padding: 0 10px;
                        border: 1px solid var(--border); border-radius: 8px;
                        background: var(--surface); color: var(--ink);
                        font: inherit; font-size: 12.5px; }
.review-ask-row input:focus { outline: none; border-color: var(--accent); }
.review-local { margin: 7px 0 0; color: var(--faint); font-size: 11px;
                line-height: 1.45; }
.review-candidates { display: flex; flex-direction: column; gap: 6px;
                     margin-bottom: 9px; }
.review-candidate { display: flex; align-items: center; gap: 8px;
                    padding: 7px 10px; border-radius: 10px;
                    background: var(--surface);
                    border: 1px dashed var(--border-2); }
.review-candidate-value { flex: 1; min-width: 0; overflow: hidden;
                          text-overflow: ellipsis; white-space: nowrap;
                          font-size: 12.5px; color: var(--ink); }
.review-candidate-kind { font-family: var(--font-mono); font-size: 10px;
                         letter-spacing: .06em; text-transform: uppercase;
                         color: var(--graphite); white-space: nowrap; }
.review-empty { margin: 9px 0 0; color: var(--faint); font-size: 11.5px;
                line-height: 1.45; }
.review-added { display: flex; flex-direction: column; gap: 6px; }
.review-added-row { display: flex; align-items: center; gap: 8px;
                    padding: 7px 10px; border-radius: 10px;
                    background: var(--ok-tint); color: var(--ok-ink);
                    border: 1px solid var(--ok-line); }
.review-added-value { flex: 1; min-width: 0; overflow: hidden;
                      text-overflow: ellipsis; white-space: nowrap;
                      font-size: 12.5px; }
.review-scope { margin: 8px 0 14px; color: var(--ink-3); font-size: 11.5px;
                line-height: 1.45; }

@media (prefers-reduced-motion: reduce) {
  .rail { transition: none; }
  .composer-box { transition: none; }
}

/* PDF review (§79): rendered pages with proposed markup drawn over them. The
   marks are positioned in PERCENTAGES of their own page, from rectangles the
   server measured, so the overlay tracks the image at any width without the
   client knowing anything about PDF coordinates. */
.pdf-pages { display: flex; flex-direction: column; gap: 18px; padding: 4px 0 8px; }
.pdf-page { position: relative; margin: 0; line-height: 0;
            box-shadow: 0 1px 3px rgba(0, 0, 0, .16); background: #fff; }
.pdf-page-img { display: block; width: 100%; height: auto; }
.pdf-marks { position: absolute; inset: 0; pointer-events: none; }
.pdf-marks .pm { position: absolute; pointer-events: auto; cursor: pointer;
                 border-radius: 2px; }
/* A strike is drawn through the middle of its own rectangle rather than as a
   filled band: a reviewer has to be able to READ the words being removed. */
/* The line is drawn as a child rather than an inset shadow: a shadow sized
   in percentages of a box ten pixels tall rounds away to nothing at ordinary
   zoom, and the reviewer sees a tint where they should see a strike. */
.pm-strike { background: color-mix(in srgb, var(--bad) 16%, transparent); }
.pm-strike::after { content: ""; position: absolute; left: 0; right: 0;
                    top: 50%; height: 1.5px; transform: translateY(-50%);
                    background: var(--bad); }
.pm-comment { background: color-mix(in srgb, var(--warn, #d9a441) 30%, transparent); }
.pm-insert { border-left: 2px solid var(--ok-line, #1b6b48); min-width: 2px; }
.pm-on { outline: 2px solid var(--ink); outline-offset: 1px; }
.pdf-page-note { position: absolute; left: 0; right: 0; bottom: 0; margin: 0;
                 padding: 5px 8px; font-size: 12px; line-height: 1.4;
                 background: color-mix(in srgb, var(--surface) 92%, transparent);
                 color: var(--muted); }
.pdf-proposals { list-style: none; margin: 10px 0 0; padding: 0;
                 display: flex; flex-direction: column; gap: 6px; }
.pdf-proposal { display: flex; align-items: flex-start; gap: 8px;
                padding: 8px 10px; border: 1px solid var(--line);
                border-radius: 6px; background: var(--surface); }
.pdf-proposal.off { opacity: .55; }
.pdf-proposal.on { border-color: var(--ink); }
.pdf-proposal-body { flex: 1; min-width: 0; display: flex; flex-wrap: wrap;
                     align-items: baseline; gap: 6px; text-align: left;
                     background: none; border: 0; padding: 0; cursor: pointer;
                     font: inherit; color: inherit; }
.pdf-proposal-kind { font-family: var(--font-mono); font-size: 11px;
                     letter-spacing: .06em; text-transform: uppercase;
                     color: var(--muted); }
.pdf-old { text-decoration: line-through; color: var(--bad); }
.pdf-new { color: var(--ok-line, #1b6b48); }
/* What became of a kept change. The chip carries the verdict; the sentence
   under it carries the reason, and takes the whole row so it reads as prose
   rather than as another value in the diff. */
.pdf-state { font-family: var(--font-mono); font-size: 10px; letter-spacing: .06em;
             text-transform: uppercase; padding: 1px 6px; border-radius: 999px;
             border: 1px solid var(--line); color: var(--muted);
             white-space: nowrap; }
.pdf-state-materialised { color: var(--ok-line, #1b6b48);
                          border-color: var(--ok-line, #1b6b48); }
.pdf-state-refused { color: var(--bad); border-color: var(--bad); }
.pdf-why { flex-basis: 100%; font-size: 12px; color: var(--ink-2, var(--muted));
           line-height: 1.45; }
.pm-warn { color: var(--warn, #8a5e14); }

/* The decision, as two buttons of which one is ON.

   It replaced a single chip whose LABEL flipped between "Remove" and
   "Restore", and the problem with that was never the wording: a lone control
   cannot say whether it describes the change's condition or offers an action.
   Every change starts kept, so the reader most likely to press it was the one
   who had just decided to keep it. Here the pressed half is the state, and it
   is drawn rather than deduced -- `aria-pressed` carries the same fact to a
   screen reader, so neither reader has to work it out from a verb. */
.pdf-proposal-seg { display: inline-flex; flex: 0 0 auto; align-self: center;
                    border: 1px solid var(--line); border-radius: 999px;
                    overflow: hidden; background: var(--canvas-2, transparent); }
.pdf-proposal-seg .turn-chip { border: 0; border-radius: 0; margin: 0;
                               background: transparent; color: var(--muted);
                               white-space: nowrap; }
.pdf-proposal-seg .turn-chip:hover { background: var(--hover); color: var(--ink); }
/* The ON half sits on the raised surface so the pair reads as a switch and
   not as two equal offers. Colour alone would not carry it: the whole row is
   already dimmed when the change is removed. */
.pdf-proposal-seg .turn-chip.on { background: var(--surface); color: var(--ink);
                                  font-weight: 600; }
.pdf-proposal-seg .seg-keep.on { color: var(--ok-line, #1b6b48); }

/* A change a LATER turn replaced on the same anchor. It stays on screen and
   loses its controls: the transcript is a log, so deleting the row would hide
   that the reader once asked for something else, and leaving the pair would
   offer a decision that cannot reach the file. */
.pdf-proposal.stale { opacity: .55; border-style: dashed; }
.pdf-proposal-stale { flex: 0 0 auto; align-self: center; font-size: 11px;
                      color: var(--muted); font-style: italic;
                      white-space: nowrap; }

/* THE STANDING BAR: what is on the table and the one action that ends it.
   Status and a download, never a per-change control -- those live on the rows
   in the transcript, and a second place to decide is a second place to
   disagree. It sits above the composer because the rows scroll away and the
   decision does not. */
.changes-standing { display: flex; align-items: center; gap: 12px;
                    flex-wrap: wrap; padding: 8px 12px; margin: 0 0 8px;
                    border: 1px solid var(--line); border-radius: 10px;
                    background: var(--surface); }
.changes-standing .cs-name { font-family: var(--font-mono); font-size: 12px;
                             overflow: hidden; text-overflow: ellipsis;
                             white-space: nowrap; min-width: 0; }
.changes-standing .cs-tally { font-size: 12px; color: var(--muted);
                              font-variant-numeric: tabular-nums; }
.changes-standing .cs-act { margin-left: auto; display: flex; gap: 8px; }

/* ---------------------------------------------------------------------------
   The document reads as a document (§79.10).

   The Word view drew its text straight onto the pane: no sheet, full pane
   width, zero side margin, and the UI's own 13.5px Inter. Beside it the PDF
   view showed a real rasterised page on white, so the SAME contract looked
   like a document in one format and like a settings screen in the other, and
   the format is the one thing the reader did not choose.

   So the page becomes a sheet. Scoped to `.document-shell` on purpose:
   `.doc-page`/`.dp` travel unscoped in documents.css and are shared with the
   documents page, which has its own column and must not inherit a pane's
   measure. Nothing here is page LAYOUT - no page breaks, no page numbers, no
   fake pagination. It is a surface, a measure and a type scale, which is what
   a reader needs to believe the words are the file's. */
.document-shell .doc-page {
  max-width: none;
  margin: 0;
  padding: 34px 40px 40px;
  background: var(--paper);
  border: 1px solid var(--hair);
  border-radius: 10px;
  box-shadow: 0 1px 2px rgba(16, 18, 22, 0.05),
              0 8px 22px -14px rgba(16, 18, 22, 0.22);
}

/* The measure, inside the sheet. A contract clause is prose, so the line
   length is the thing that decides whether it can be read at all: the pane is
   wide enough to run past 110 characters, which is roughly twice a comfortable
   line. Centred within its own padding rather than by shrinking the sheet, so
   the paper still fills the pane and only the TEXT is measured. */
.document-shell .doc-page > * {
  max-width: 34rem;
  margin-left: auto;
  margin-right: auto;
}

.document-shell .dp {
  font-size: 15px;
  line-height: 1.72;
  margin: 0 0 15px;
}

/* A heading scale with real steps in it. The old one ran 19 / 16.5 / 15 / 14
   against 13.5px body, so an h1 and an h4 were four points apart across four
   levels and the hierarchy was a claim rather than something the eye could
   use. */
.document-shell .dp-h { margin-top: 30px; margin-bottom: 9px;
                        line-height: 1.28; letter-spacing: -0.006em; }
.document-shell .dp-h1 { font-size: 25px; margin-top: 6px; }
.document-shell .dp-h2 { font-size: 19px; }
.document-shell .dp-h3 { font-size: 16.5px; }
.document-shell .dp-h4, .document-shell .dp-h5, .document-shell .dp-h6,
.document-shell .dp-h7, .document-shell .dp-h8, .document-shell .dp-h9 {
  font-size: 15px; font-weight: 650;
}
/* A heading that opens the document has nothing above it to be separated
   from, and the gap read as a missing element. */
.document-shell .doc-page > .dp-h:first-child { margin-top: 0; }

/* Lists and tables are content the file really carries, so they get the same
   care as the paragraphs rather than falling back to browser defaults. */
.document-shell .dp-li { margin-bottom: 7px; line-height: 1.7; }
.document-shell .doc-page ul, .document-shell .doc-page ol { padding-left: 26px;
                                                             margin: 0 0 15px; }
.document-shell .doc-page table { width: 100%; border-collapse: collapse;
                                  margin: 4px 0 18px; font-size: 14px; }
.document-shell .doc-page th, .document-shell .doc-page td {
  border: 1px solid var(--hair); padding: 7px 10px; text-align: left;
  vertical-align: top;
}
.document-shell .doc-page th { font-weight: 650; background: var(--layer2); }

/* The caveat is a footnote, not a paragraph of the document. It sat directly
   under the last clause at body width and read as the contract's own final
   section. */
.document-shell .changes-caveat { max-width: 34rem; margin: 14px auto 0;
                                  line-height: 1.5; opacity: 0.85; }

@media (max-width: 900px) {
  .document-shell .doc-page { padding: 22px 20px 28px; border-radius: 8px; }
  .document-shell .doc-page > *, .document-shell .changes-caveat { max-width: none; }
}

/* The PDF's selectable text (§79.11). Invisible words laid exactly over the
   drawn ones, so the reader selects what they see on a page that is a picture.

   `color: transparent` and not `opacity: 0`, `visibility: hidden` or
   `display: none`: the last two take the node out of the selection entirely,
   which is the whole point of the layer, and opacity leaves it painting a hit
   target over the marks. Transparent text still selects, still copies, and
   still shows the browser's own selection highlight through to the pixels
   underneath.

   Each line is absolutely positioned from the server's rectangle. The font is
   never matched to the document's: it does not need to be, because nothing
   here is seen. What matters is that the box is the right box, so the
   selection highlight lands on the right words. */
.pdf-text { position: absolute; inset: 0; z-index: 2; }
/* The page is the unit the layer measures against. `inline-size` and not
   `size`: the figure's height comes from the image inside it, and a size
   container may not take its height from its own content. */
.document-shell .pdf-page { container-type: inline-size; }
.pdf-line {
  position: absolute;
  color: transparent;
  white-space: pre;
  transform-origin: 0 0;
  cursor: text;
  /* The line is sized by its measured box, and the glyphs inside it are
     stretched to fill that box rather than trusted to be the same width. A
     document set in a font the browser does not have would otherwise drift
     across the line and the selection would highlight the wrong words. */
  font: 400 1em/1 ui-sans-serif, system-ui, sans-serif;
  overflow: hidden;
}
/* The highlight, and ONLY the highlight. `color: transparent` on the element
   is not enough: a browser paints SELECTED text in the selection's own
   foreground colour, which overrides it, so the invisible layer became
   visible the moment it was selected and every selected line showed doubled
   glyphs over the drawn ones. Restating transparent inside ::selection keeps
   the band and drops the letters, which is what makes the highlight look like
   it belongs to the page underneath. */
.pdf-line::selection { background: rgba(64, 120, 220, 0.30); color: transparent; }
.pdf-line::-moz-selection { background: rgba(64, 120, 220, 0.30); color: transparent; }

/* The marks sit above the text layer but must never eat a selection that
   starts on a word underneath one. Only the mark itself is clickable. */
.document-shell .pdf-marks { z-index: 3; pointer-events: none; }
.document-shell .pdf-marks .pm { pointer-events: auto; }

/* ---------------------------------------------------------------- agent ---
   §80. Agent mode is a MODE and not a source, so its control is its own button
   beside Sources rather than a fourth row inside it: a source says where an
   answer may be drawn FROM, this says how the next message is handled.

   Latched, it wears the accent the way a live source chip does: one glance
   has to answer "is the next message going to be run as a task". */

/* The run's account of itself, under the answer and above the model chip. */
/* A BOTTOM MARGIN BECAUSE IT PRECEDES THE ANSWER. A top margin alone was
   right while the run's account was drawn underneath; above, with nothing
   under it, `library.list_documents  Cedreon document library` and the reply
   sat one line apart and the answer read as a third row of the list. 14px is
   the receipt's own bottom gap (`.activity`), because the two blocks are the
   same kind of thing in the same position. */
.agent-run { margin: 12px 0 14px; }
/* THE SAME SPINE AS THE LIVE SURFACE. The reader watched a vertical timeline
   run and this is the record of that same run, so drawing it as a different
   kind of object made one turn look like two accounts. Same column, same
   hairline, same rule that the line stops at the last node -- what differs is
   that nothing here is live, so there is no pulse and no row without a mark. */
/* NO PROGRESS LIST AND NO TECHNICAL FOLD. `.agent-steps`, `.agent-step*`,
   `.agent-tech-fold`, `.agent-tech-actions`, `.agent-tool`, `.agent-dest` and
   `.agent-summary-note` used to live here, drawing the run's operations a
   second time as machine states and tool ids.

   They were moved behind a shut `<details>` first, which is why the note is
   worth leaving: a fold is not a removal. The rows stayed one press away on
   the product's main surface, and what the press revealed was
   `library.search  Cedreon document library` -- exactly what the fold was
   built to get out of the reader's way. The receipt (`.agent-did`) is the
   whole of what this surface says now; the calls themselves are in the hash
   chain, which is the instrument that audience actually wants and the one
   nobody can delete. See `Agent.tsx`'s header. */

/* THE RECEIPT, AT THE SIZE OF A CITATION. It had NO rules at all -- the
   component shipped a bare `<ul>`, so the browser drew disc bullets at body
   size, above the answer, and a run that read three files opened its own reply
   with three bulleted filenames. `.sources` is on the wrapper, so the spacing
   comes from the same rule the web and Drive notes use and there is no second
   number to keep in step.

   NO KICKER. §103.3 removed "What Cedreon did" because the list already reads
   as one; a label over it here would put the heading back one register down.
   The rows are quiet enough to be skipped and precise enough to be checked,
   which is what provenance under an answer is for. */
.agent-provenance .agent-did-list { list-style: none; margin: 0; padding: 0;
                                    display: flex; flex-direction: column;
                                    gap: 2px; }
.agent-provenance .agent-did-what,
.agent-provenance .agent-did-open { font-size: 12px; line-height: 17px;
                                    color: var(--faint); }
/* A row is a control only where the server's record named a target, so the
   two faces are told apart by the ONE thing that differs: whether it can be
   pressed. Same size, same colour at rest -- a receipt should not look like a
   toolbar -- and the affordance appears on hover and focus. */
.agent-provenance .agent-did-open { display: inline; margin: 0; padding: 0;
                                    border: 0; background: none; font: inherit;
                                    text-align: left; cursor: pointer;
                                    border-bottom: 1px solid transparent; }
.agent-provenance .agent-did-open:hover,
.agent-provenance .agent-did-open:focus-visible { color: var(--ink-2);
                                    border-bottom-color: var(--border); }

/* SEVERAL READS ARE ONE LINE. Four documents drew four rows of the same verb
   above the answer; the verb is said once now, with a count and the filenames
   beside it. The names are still each a control, so nothing is folded away --
   what left is the repetition, not the reach. Wraps rather than clipping: a
   receipt that hides which file it read is the defect §103.2a fixed. */
.agent-provenance .agent-did-reads { display: flex; align-items: baseline;
                                    flex-wrap: wrap; gap: 4px 6px; }
/* The count reads at a glance and is `aria-hidden` -- the row's own
   `aria-label` says the whole sentence, so this is a mark and not a word. */
.agent-provenance .agent-did-count { font-size: 11px; line-height: 15px;
                                    min-width: 15px; padding: 0 4px;
                                    border-radius: 999px;
                                    background: var(--hover);
                                    color: var(--faint);
                                    text-align: center;
                                    font-variant-numeric: tabular-nums; }
.agent-provenance .agent-did-names { display: flex; flex-wrap: wrap;
                                    gap: 2px 8px; min-width: 0; }
/* The same two faces as `.agent-did-open`, at the same weight: a receipt is
   not a toolbar, so the affordance appears on hover and focus. The button
   reset is spelled out because a `span` that becomes a `button` inherits the
   UA's border, font and cursor otherwise. */
.agent-provenance .agent-did-chip { font-size: 12px; line-height: 17px;
                                    color: var(--faint); display: inline;
                                    margin: 0; padding: 0; border: 0;
                                    background: none; font-family: inherit;
                                    text-align: left;
                                    border-bottom: 1px solid transparent; }
.agent-provenance button.agent-did-chip { cursor: pointer; }
.agent-provenance button.agent-did-chip:hover,
.agent-provenance button.agent-did-chip:focus-visible { color: var(--ink-2);
                                    border-bottom-color: var(--border); }

/* THE APPROVAL CARD. Deliberately the loudest thing in the turn: it is the one
   moment the reader is being asked to authorise something, and a control that
   changes a file or leaves the building may not look like a footnote. */
.agent-approval { margin-top: 12px; padding: 12px 14px;
                  border: 1px solid var(--warn); border-radius: 12px;
                  background: var(--warn-tint); }
.agent-approval-head { display: flex; align-items: center; flex-wrap: wrap;
                       gap: 8px; margin-bottom: 8px; }
.agent-badge { font-family: var(--font-mono); font-size: 10px;
               letter-spacing: .07em; text-transform: uppercase;
               color: var(--warn); font-weight: 600; }
.agent-cap { font-size: 12.5px; color: var(--ink-2); }
.agent-summary { margin: 4px 0 10px; font-size: 13.5px; color: var(--ink); }
.agent-facts { margin: 0; display: grid; grid-template-columns: auto 1fr;
               gap: 4px 12px; align-items: baseline; font-size: 12.5px; }
.agent-facts dt { font-family: var(--font-mono); font-size: 10px;
                  letter-spacing: .07em; text-transform: uppercase;
                  color: var(--faint); }
.agent-facts dd { margin: 0; color: var(--ink-2); min-width: 0;
                  overflow-wrap: anywhere; }
.agent-args { list-style: none; margin: 0; padding: 0;
              display: flex; flex-direction: column; gap: 3px; }
.agent-args li { display: flex; gap: 8px; min-width: 0; }
.agent-args b { flex: none; font-family: var(--font-mono); font-size: 11.5px;
                font-weight: 500; color: var(--faint); }
.agent-args span { min-width: 0; overflow-wrap: anywhere;
                   /* Bounded: an approval card must show the reader what will
                      run, not become the document. */
                   max-height: 7.5em; overflow: auto; }
.agent-actions { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }

@media (max-width: 560px) {
  /* The two decisions stay side by side and full width, so neither is the
     easier press by accident. */
  .agent-actions { flex-direction: column; }
  .agent-actions .btn { width: 100%; justify-content: center; }
}

/* The matter this chat is in (§89), in the line under the composer. Quiet by
   design: it is a statement about scope, not a control, so it carries the
   note's own colour and only its weight sets it apart from the disclaimer
   beside it. */
.composer-scope { font-weight: 500; color: var(--ink-2, var(--muted)); }
.composer-scope-sep { opacity: .55; }
