/* ─────────────────────────────────────────────────────────────
   Cotrugli — Components
   Token-driven, framework-free recreations of the kit primitives.
   Each block maps 1:1 to a component in the design bundle's
   primitives.jsx, so it can become a Phoenix function component later.
   ───────────────────────────────────────────────────────────── */

/* ── Icon — Tabler outline, inherits color, 1.5 stroke (see sprite) ── */
.icon { width: 18px; height: 18px; flex-shrink: 0; display: inline-block; vertical-align: middle; }
.icon--12 { width: 12px; height: 12px; }
.icon--14 { width: 14px; height: 14px; }
.icon--16 { width: 16px; height: 16px; }

/* ── Section navigation ──────────────────────────────────────────────── */
.section-nav { position: relative; min-width: 0; color: var(--text); font: var(--t-body); }
.section-nav__row { display: flex; align-items: stretch; min-height: 48px; border-bottom: 1px solid var(--surface-rule); }
.section-nav__groups { display: flex; align-items: stretch; gap: var(--sp-1); min-width: 0; flex: 1 1 auto; }
.section-nav__group { position: relative; display: flex; align-items: stretch; flex: 0 0 auto; }
.section-nav__group--utility { margin-inline-start: auto; }
.section-nav__link, .section-nav__group-trigger, .section-nav__home-link, .section-nav__mobile-trigger {
  min-height: 48px; padding: 0 var(--sp-3); display: inline-flex; align-items: center; gap: var(--sp-2);
  border: 0; background: transparent; color: var(--text); font: var(--t-body); text-decoration: none; white-space: nowrap; cursor: pointer;
}
.section-nav__link:hover, .section-nav__group-trigger:hover, .section-nav__home-link:hover { background: var(--surface-hover); }
.section-nav__link:focus-visible, .section-nav__group-trigger:focus-visible, .section-nav__home-link:focus-visible, .section-nav__mobile-trigger:focus-visible, .section-nav__sheet-close:focus-visible, .section-nav__menu-link:focus-visible, .section-nav__sheet-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.section-nav__link--active, .section-nav__group-trigger--active { border-bottom: 2px solid var(--text); }
.section-nav__group-trigger .icon { transition: transform var(--t-quick) var(--easing); }
.section-nav__group-trigger[aria-expanded="true"] .icon { transform: rotate(180deg); }
.section-nav__menu { position: absolute; z-index: var(--z-menu); top: calc(100% - 1px); left: 0; min-width: 220px; max-height: min(70dvh, 520px); overflow-y: auto; padding: var(--sp-2); border: 1px solid var(--surface-rule); border-radius: var(--r-3); background: var(--surface-card); }
.section-nav__menu[hidden] { display: none; }
.section-nav__menu-link, .section-nav__sheet-link { min-height: 44px; padding: var(--sp-2) var(--sp-3); display: flex; align-items: center; color: var(--text); text-decoration: none; border-radius: var(--r-2); }
.section-nav__menu-link:hover, .section-nav__sheet-link:hover { background: var(--surface-hover); }
.section-nav__menu-link--active, .section-nav__sheet-link--active { font-weight: 550; }
.section-nav__compact-home, .section-nav__mobile-trigger, .section-nav__mobile-label, .section-nav__sheet { display: none; }
.section-nav[data-compact="true"] .section-nav__mobile-trigger .section-nav__mobile-label { display: inline; }
.section-nav__desktop-label { display: inline; }
.section-nav[data-compact="true"] .section-nav__groups { display: none; }
.section-nav[data-compact="true"] .section-nav__compact-home, .section-nav[data-compact="true"] .section-nav__mobile-trigger { display: inline-flex; }
.section-nav__sheet:not([open]) { display: none; }
.section-nav__sheet[open] { display: block; max-height: min(86dvh, 720px); overflow-y: auto; }
.section-nav__sheet { border: 0; margin: auto; padding: var(--sp-4); width: min(100vw, 460px); max-width: 100%; color: var(--text); background: var(--surface); }
.section-nav__compact-home { flex: 0 1 auto; min-width: 0; max-width: 50%; }
.section-nav__home-link { white-space: normal; overflow-wrap: anywhere; }
.section-nav__mobile-trigger { flex: 1; min-width: 0; justify-content: space-between; white-space: normal; text-align: start; }
.section-nav__mobile-label { min-width: 0; overflow-wrap: anywhere; }
.section-nav__sheet-link, .section-nav__menu-link { overflow-wrap: anywhere; }
.section-nav__menu { max-width: calc(100vw - 16px); }
.section-nav__sheet::backdrop { background: var(--scrim); }
.section-nav__sheet-head { min-height: 44px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--surface-rule); font-weight: 550; }
.section-nav__sheet-close { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; border: 0; background: transparent; color: var(--text); cursor: pointer; }
.section-nav__sheet-group { padding-block: var(--sp-3); }
.section-nav__sheet-group h2 { margin: 0 0 var(--sp-1); font: var(--t-body-sm); color: var(--text-muted); }
@media (max-width: 760px) {
  .section-nav__groups, .section-nav__compact-home { display: none; }
  .section-nav__row { min-height: 48px; }
  .section-nav__mobile-trigger { display: inline-flex; justify-content: space-between; width: 100%; text-align: start; }
  .section-nav__sheet[open] { display: block; position: fixed; inset: auto 0 0; margin: 0; max-width: none; width: auto; }
}
@media (prefers-reduced-motion: reduce) { .section-nav__group-trigger .icon { transition: none; } }

/* ── Brand - the counterpart symbol and the Source Serif 4 wordmark, drawn
   from the canonical geometry (priv/static/images/brand, vendored from the
   approved pack). One colour, currentColor, so both themes wear the same
   artwork; no tile, no typed C, no rebuilt kerning. Minimums are the brand
   guide's: a lockup from 160px wide, a wordmark from 120px. ── */
.brand { display: inline-block; color: var(--text); flex-shrink: 0; vertical-align: middle; }
.brand--symbol { width: 28px; height: 28px; }
/* Production clamp of the brand minimums: dev and test raise instead (see
   check_minimum!/2, CoreComponents), but a production render can only ever
   be sized at or above the brand guide's floor here. */
.brand--wordmark { width: 120px; height: auto; min-width: 120px; }
.brand--lockup { width: 160px; height: auto; min-width: 160px; }
/* Arrival, on the threshold surfaces (login, the gate) and the front-door
   greeting's mark: both parts stroke themselves in over 620ms, then the
   fill floods on the same beat, so the draw settles into the symbol.
   Reduced motion renders it settled. */
.brand-arrival__part { fill-opacity: 0; stroke-dasharray: 1; stroke-dashoffset: 1; animation: brand-draw 620ms var(--ease-out) 120ms forwards, brand-fill 320ms var(--easing) 580ms forwards; }
@keyframes brand-draw { to { stroke-dashoffset: 0; } }
@keyframes brand-fill { to { fill-opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .brand-arrival__part { stroke-dashoffset: 0; fill-opacity: 1; animation: none; } }

/* ── Auth threshold: the sign-in, refusal and recovery pages share one
   threshold; the gate (priv/static/ds/gate.css) is the public twin. ── */
.auth-shell {
  min-height: 100vh; /* fallback for browsers without dvh support */
  min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-6); text-align: center; padding-inline: var(--pad-view);
}
.auth-shell__brand { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); text-decoration: none; color: var(--text); }
.auth-shell__title { margin-top: var(--sp-2); }
.auth-shell__sub { margin-top: var(--sp-3); max-width: 44ch; }
.auth-shell__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-3); margin-top: var(--sp-2); }
/* The threshold's verbs meet the touch floor everywhere, even where the
   surrounding row does not otherwise need it. */
.auth-shell .btn { min-height: var(--tap-min); padding-inline: var(--sp-5); }

/* ── Lockup — mark + wordmark (chrome; never underlined) ── */
.lockup { display: inline-flex; align-items: center; gap: 14px; text-decoration: none; color: var(--text); }

/* ── Button — five flavors, tokenized. Hovers LIGHTEN (one step, never a
   dim); press is a transform on the base transition so it lands instantly
   and eases on release. ── */
.btn {
  font-family: var(--font-sans); font-size: 13px; font-weight: 500;
  padding: 8px 14px; border-radius: var(--r-2); border: 1px solid transparent;
  cursor: pointer; display: inline-flex; align-items: center; gap: 8px;
  line-height: 1; white-space: nowrap; text-decoration: none;
  transition: background-color var(--t-base) var(--easing),
              border-color var(--t-base) var(--easing),
              color var(--t-base) var(--easing),
              transform var(--t-quick) var(--ease-out);
}
.btn--lg { font-size: 14px; padding: 10px 18px; }   /* marketing scale */

.btn--primary   { background: var(--text); color: var(--surface); }
.btn--primary:hover   { background: var(--text-hover); }
.btn--secondary { background: transparent; border-color: var(--surface-rule); color: var(--text); }
.btn--secondary:hover { border-color: var(--text-subtle); background: var(--surface-hover); }
.btn--ghost     { background: transparent; border-color: var(--surface-rule); color: var(--text-muted); }
.btn--ghost:hover     { border-color: var(--text-subtle); background: var(--surface-hover); color: var(--text); }
.btn--danger    { background: transparent; border-color: var(--danger); color: var(--danger-text); }
/* color-mix keeps the danger wash honest on any surface it sits on. */
.btn--danger:hover    { background: color-mix(in srgb, var(--danger) 10%, transparent); }
/* The composer's send-to-agent control, and nothing else (the Signal Rule):
   this is the one chartreuse HUMAN control in the product, because sending
   to the agent is the act that starts evidenced agent work. Never reused
   for a proposal's Approve or any other person's decision - those are
   neutral (.btn--primary/.btn--secondary). */
.btn--agent     { background: var(--signal); color: var(--signal-ink); font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.02em; }
.btn--agent:hover     { background: var(--signal-soft); }

/* ── Icon actions ────────────────────────────────────────────────────────
   One neutral native control across Console, Practice, Client and auth.
   Compact and regular describe artwork density; the interactive box keeps
   the 44px house floor at every viewport. Callers own only placement. */
.icon-btn {
  position: relative;
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  height: var(--tap-min);
  min-height: var(--tap-min);
  border: 1px solid transparent;
  border-radius: var(--r-2);
  background: transparent;
  color: var(--text-muted);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    background-color var(--t-base) var(--easing),
    border-color var(--t-base) var(--easing),
    color var(--t-base) var(--easing),
    opacity var(--t-base) var(--easing),
    transform var(--t-quick) var(--ease-out);
}
.icon-btn--compact { padding: 0 var(--sp-2); }
.icon-btn--regular { padding: 0 var(--sp-3); }
.icon-btn--outline { border-color: var(--surface-rule); }
@media (hover: hover) {
  .icon-btn--quiet:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--surface-rule); color: var(--text); }
  .icon-btn--outline:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--text-subtle); color: var(--text); }
}
.icon-btn--quiet:focus-visible { background: var(--surface-hover); border-color: var(--surface-rule); color: var(--text); }
.icon-btn--outline:focus-visible { background: var(--surface-hover); border-color: var(--text-subtle); color: var(--text); }
.icon-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.icon-btn[aria-busy="true"],
.icon-btn.phx-click-loading { opacity: 0.55; cursor: progress; pointer-events: none; }

/* ── Segmented choices ─────────────────────────────────────────────────
   One labelled native-button group for Console and Practice. The selected
   option carries weight and a strong inset boundary as well as a neutral
   surface step, so colour is never its only evidence. Compact rows scroll
   inside their own width; explained choices stack their supporting prose. */
.switcher {
  display: inline-flex;
  gap: 2px;
  max-width: 100%;
  padding: 3px;
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-4);
  background: var(--surface-elev);
}
.switcher--compact { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; }
.switcher--compact .switcher__btn {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
}
.switcher__btn {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0 var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--r-3);
  background: transparent;
  color: var(--text-muted);
  font: var(--t-body-sm);
  font-weight: 500;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    background-color var(--t-base) linear,
    border-color var(--t-base) linear,
    color var(--t-base) linear,
    transform var(--t-quick) var(--ease-out);
}
@media (hover: hover) {
  .switcher__btn:hover:not(:disabled) { background: var(--surface-hover); color: var(--text); }
}
.switcher__btn:focus-visible { background: var(--surface-hover); }
.switcher__btn[aria-pressed="true"],
.switcher__btn--active {
  background: var(--surface-overlay);
  color: var(--text);
  box-shadow: 0 0 0 1px var(--line-strong);
  font-weight: 600;
}
.switcher__btn:disabled { opacity: 0.45; cursor: not-allowed; }
.switcher__description { display: block; color: var(--text-faint); font: var(--t-micro); font-weight: 400; }
.switcher__btn[aria-pressed="true"] .switcher__description { color: var(--text-muted); }
.switcher--explained { display: flex; width: 100%; overflow: hidden; }
.switcher--explained .switcher__btn {
  flex: 1 1 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  white-space: normal;
}
@media (max-width: 900px) {
  .switcher--explained { flex-direction: column; }
  .switcher--explained .switcher__btn { flex: none; width: 100%; }
}

/* Pressed: every button answers the finger, instantly. The same press extends
   to the segmented, tab, and pill controls (their transforms ease back on
   release; see each control's transition). Wide list rows are left out - a
   scale on a full-width row reads as a wobble, not a press. */
.btn:active, .icon-btn:active,
.client-tab:active, .switcher__btn:active, .tenant-option:active,
.tb__ctl:active, .approvals-new-pill:active { transform: translateY(0.5px) scale(0.98); }
@media (prefers-reduced-motion: reduce) {
  .btn:active, .icon-btn:active,
  .client-tab:active, .switcher__btn:active, .tenant-option:active,
  .tb__ctl:active, .approvals-new-pill:active { transform: none; }
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* Colored flavors also drain: opacity alone still reads clickable. */
.btn--primary:disabled, .btn--danger:disabled, .btn--agent:disabled { filter: saturate(0); }
/* In-flight (phx-click-loading): the label steps back but keeps its width —
   the button never jumps layout while the round-trip runs. */
.btn--secondary.phx-click-loading, .btn--ghost.phx-click-loading, .btn--danger.phx-click-loading { color: var(--text-muted); }
.btn--primary.phx-click-loading, .btn--agent.phx-click-loading { opacity: 0.75; }
/* Pending, the `aria-busy` twin of `phx-click-loading` (same intent, the
   attribute a screen reader also gets): cursor answers first, the word
   steps back, and a 12px currentColor ring spinner leads it. */
.btn[aria-busy="true"] { cursor: progress; color: var(--text-subtle); }
.btn--primary[aria-busy="true"] { color: var(--surface); opacity: 0.75; }  /* keeps the inverse word readable while pending */
.btn[aria-busy="true"]::before {
  content: "";
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: btn-spin 600ms linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn[aria-busy="true"]::before { animation: none; }
}

/* ── Arrow link — one vocabulary for "this navigates somewhere" ──
   Composes onto an existing link class; the arrow nudges 2px on hover
   (a state change on the system's quick token, stilled under
   prefers-reduced-motion). */
.arrow-link { display: inline-flex; align-items: center; gap: 6px; }
.arrow-link .icon { flex: none; transition: transform var(--t-quick) var(--ease-out); }
.arrow-link:hover .icon { transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) {
  .arrow-link:hover .icon { transform: none; }
}

/* ── Badge — agent + four states + neutral ── */
.badge {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tr-wide);
  padding: 3px 7px; border-radius: var(--r-1); display: inline-flex;
  align-items: center; gap: 6px; text-transform: uppercase; line-height: 1.4;
  border: 1px solid transparent;
}
.badge__dot { width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
/* The status glyph (CotrugliWeb.StatusGlyph): one SHAPE per status beside its
   hue, so a status survives a reader who cannot separate amber from red. It
   takes the dot's place wherever a status word already wore one, and sizes in
   em so it tracks whatever type it sits beside - 10px in a badge, the row's
   own size in a list. It is a line drawing in currentColor: no colour is
   named here, and none moves. */
.glyph { width: 1em; height: 1em; vertical-align: -0.15em; flex: none; }
/* A severity badge may lead with a glyph instead of the dot: shape as well as
   colour, so the tier reads without relying on colour alone. The glyph takes
   the badge's own colour (currentColor). */
.badge--icon { gap: 5px; }
.badge__icon { width: 12px; height: 12px; flex: none; margin: -1px 0; }
.badge--agent   { background: var(--surface); color: var(--text); border-color: var(--surface-rule); }
.badge--agent .badge__dot { background: var(--signal-mark); }
/* Every state badge paints its own --surface-elev, so the badge fill is the
   governing pair for all five of them, at the 10px this type is set in. Swept
   by computation in WarningContrastTest (oklch -> sRGB -> WCAG, both themes):
   danger, info and neutral clear 4.5:1 as fills; success and warning did not
   on paper (4.48 and 4.49), so each has a TYPE token deepened on L with hue
   and chroma held. Every badge--success and badge--warning in the product is
   the same pair, so pointing these two rules at the ink fixes every surface
   that wears one - no markup moved. */
.badge--success { background: var(--surface-elev); color: var(--success-text); border-color: var(--surface-rule); }
.badge--warning { background: var(--surface-elev); color: var(--warning-text); border-color: var(--surface-rule); }
.badge--danger  { background: var(--surface-elev); color: var(--danger-text);  border-color: var(--surface-rule); }
.badge--info    { background: var(--surface-elev); color: var(--info-text);    border-color: var(--surface-rule); }
.badge--neutral { background: var(--surface);      color: var(--text-muted); border-color: var(--surface-rule); }
/* CE-07/COT-586: the outlined chip an absent evidence-trail hop wears -
   border only, no fill, the "not on file" counterpart to every filled
   variant above. */
.badge--outline { background: transparent; color: var(--text-muted); border-color: var(--surface-rule); }

/* ── Status: a word and a shape (F1 proofs), never a hue alone. Only the
   Recorded variant is built out here: the settled, evidenced state a
   proposal record actually confirms happened. The rest of the F1
   vocabulary (proposed/pending/done/etc.) is not yet consumed anywhere in
   the product and stays out until it is. ── */
/* No font declared here: the word's own type role (e.g. .cot-label's mono
   uppercase treatment) must win undisturbed when .status rides alongside
   it; a bare .status with no such role simply inherits the ambient font. */
.status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 8px; border: 1px solid var(--surface-rule); border-radius: var(--r-2); color: var(--text); white-space: nowrap; }
.status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid currentColor; flex: none; }
/* The Signal Rule: action marks evidenced agent activity or authorship. A
   settled, evidenced act takes the signal as its border and its filled
   mark - never a bright fill behind the word, which stays neutral. */
.status--recorded { border-color: var(--signal); }
.status--recorded::before { background: var(--signal); border-color: var(--signal); }

/* ── Section tag — "§ 01: label", mono, neutral text with a signal kicker
   tick (marketing exception: a tick, never a painted label). ── */
.section-tag {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tr-label);
  color: var(--text-subtle); text-transform: uppercase; margin-bottom: 12px;
  display: flex; align-items: center; gap: 8px;
}
.section-tag::before {
  content: ""; width: 6px; height: 6px; background: var(--signal); flex-shrink: 0;
}

/* ── Stat — big mono number + label (tiles) ── */
.stat { display: flex; flex-direction: column; gap: 6px; }
.stat__label { font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--text-subtle); }
.stat__value { font-family: var(--font-mono); font-size: 26px; font-weight: 500; color: var(--text); letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.stat__sub   { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); font-variant-numeric: tabular-nums; }

/* ── Cards — borders, not boxes. No shadows, anywhere. ── */
.card          { background: var(--surface-card); border: 1px solid var(--surface-rule); border-radius: var(--r-3); }
.card--accent  { border-left: 2px solid var(--signal); border-radius: 0 var(--r-3) var(--r-3) 0; }  /* agent content only */

/* ── Data table — the signature surface ──
   Separate borders, not collapsed: a collapsed border belongs to the grid,
   so it scrolls out from under a sticky header. Every rule in the table is
   a border-bottom on its own cell (never a top), so nothing doubles. */
.data-table { width: 100%; font-size: 13px; border-collapse: separate; border-spacing: 0; }
.data-table th {
  text-align: left; font-family: var(--font-mono); font-size: 10px;
  letter-spacing: var(--tr-label); color: var(--text-subtle); text-transform: uppercase;
  padding: 10px 14px; border-bottom: 1px solid var(--surface-rule); font-weight: 500;
}
.data-table td { padding: 11px 14px; border-bottom: 1px solid var(--surface-rule); color: var(--text); vertical-align: middle; }
/* Edge cells take the panel gutter, so the first column's left edge lands
   exactly under the panel head's text (14px inter-column rhythm stays). */
.data-table th:first-child, .data-table td:first-child { padding-left: var(--pad-panel); }
.data-table th:last-child, .data-table td:last-child { padding-right: var(--pad-panel); }
.data-table--flush tbody tr:last-child td { border-bottom: none; }   /* BAS / numbers tables drop the final rule */
.data-table .num   { font-family: var(--font-mono); text-align: right; font-variant-numeric: tabular-nums; }
.data-table .mono  { font-family: var(--font-mono); }
.data-table .muted { color: var(--text-muted); }
.col-right { text-align: right; }
/* Beats .data-table th { text-align: left }. A bare .col-right loses, so
   SOURCE sits on the left of its column while the figure sits on the right
   and reads as the next header's (Sworn 1010 on a wide desktop). */
.data-table th.col-right { text-align: right; }

/* ── Money — the decorated figure (Cotrugli.Display.Money renders it) ──
   A money value carries its own markup wherever it lands, table cell or
   sentence, so the vocabulary is three classes and no layout of its own.
   A negative wears the ledger's brackets in the markup and NOTHING here:
   half a chart of accounts is legitimately in credit, and a page of red
   figures reads as a page of errors. The brackets carry the meaning; a
   cell that is genuinely an alarm opts into red at the cell level
   (td.num--alert), never through the sign. money--neg stays in the
   markup as the semantic hook for exactly that kind of opt-in.
   The currency code trails the figure as a quieter, spaced sibling, so
   the eye reads the number first and the unit second. */
.money       { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.money__ccy  { font-size: 0.8em; color: var(--text-muted); margin-left: 0.45em; letter-spacing: 0.05em; }

/* A surface whose every figure shares one currency says it once in its
   meta line and quiets the per-cell code: four AUD chips per statement
   row restate a fact the page already stated. The code stays in each
   figure's title and in the DOM for the audit eye; per-row-currency
   tables (reconcile, AR/AP) never take this class. */
.one-currency .money__ccy { display: none; }

/* Reconciliation row hover — a quiet lift, no color. Hovers LIGHTEN,
   never recess: a darker row reads as a hole in the panel. */
.data-table--rows tbody tr { transition: background-color var(--t-quick) var(--easing); }
.data-table--rows tbody tr:hover { background: var(--surface-hover); }

/* ── Tooltip — [data-tip] ──
   Floating chrome, so the overlay physics apply (surface step + hairline +
   the one sanctioned shadow). Fade only (linear), delayed on the way in so
   passing the pointer across chrome never flashes copy. The host keeps its
   own aria-label/text; the tip is the visual layer. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  z-index: var(--z-tip);
  width: max-content; max-width: 240px; white-space: normal;
  background: var(--surface-overlay); border: 1px solid var(--surface-rule);
  border-radius: var(--r-2); box-shadow: var(--shadow-overlay);
  padding: 6px 10px;
  font: 400 11px/1.45 var(--font-sans); letter-spacing: normal;
  text-transform: none; text-align: left; color: var(--text);
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-quick) var(--easing);
}
[data-tip]:hover::after { opacity: 1; transition-delay: 300ms; }
[data-tip]:focus-visible::after { opacity: 1; }
/* Placement, space-separated in data-tip-pos: hosts at the top of a clipping
   container (topbar, panel heads) drop the tip below; hosts against a right
   edge pin it to their right end so it grows into open space, not the clip. */
[data-tip-pos~="down"]::after { bottom: auto; top: calc(100% + 6px); }
[data-tip-pos~="end"]::after { left: auto; right: 0; transform: none; }

/* ── Keycap — .kbd ──
   The machined key: a top-lit cap seated in a dark well. This gradient is
   the ONE sanctioned gradient beyond chart fills — the keycap: a physical
   object, not a surface (keyboard hardware, carved out the way the
   chart-fill fade is). Nothing else in the chrome may borrow it. */
.kbd {
  display: inline-flex; align-items: center; justify-content: center;
  height: 20px; padding: 1px 6px; border-radius: var(--r-2);
  background: linear-gradient(180deg, var(--surface-overlay), var(--surface-elev));
  color: var(--text-muted);
  font: 500 10px/1 var(--font-mono);
  box-shadow:
    0 1.5px 0.5px rgba(0, 0, 0, 0.4),
    0 0 0.5px 1px rgba(0, 0, 0, 0.9),
    inset 0 2px 1px rgba(0, 0, 0, 0.2),
    inset 0 1px 1px rgba(255, 255, 255, 0.12);
}
.light .kbd {
  /* The light overlay token IS white; the keycap reads it rather than spelling it. */
  background: linear-gradient(180deg, var(--surface-overlay), var(--surface-elev));
  box-shadow:
    0 1px 0.5px rgba(0, 0, 0, 0.15),
    0 0 0 1px rgba(0, 0, 0, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);
}

/* The theme control is `.theme-cycle__*`, moved here beside the shared account
   foot that renders it, and there is no `.theme-toggle` anywhere in this
   app any more: the two-state button keyed on `html.light` was replaced by
   the one row that can say System (2026-08-30, Phase 3), and its rules
   outlived its markup by a commit. The static kits under `workspace/` carry
   their own copy of that button and their own components.css - they are not
   this shell and are not served from here. */

/* ── Flash — quiet session-moment notice, fixed top-right ──
   Floating chrome, so the overlay physics apply: overlay surface + hairline
   + the one sanctioned shadow. Arrival is a quiet settle in from the right
   edge (a fade riding a nudge; base.css stills it under reduced motion).
   Info notices dismiss themselves after 6s via the colocated .FlashTimer
   hook, paused while the pointer or focus is on the card; errors persist. */
.flash-stack {
  position: fixed; top: var(--sp-4); right: var(--sp-4); z-index: var(--z-chrome);
  display: flex; flex-direction: column; gap: var(--sp-2);
  width: 320px; max-width: calc(100vw - 2 * var(--sp-4));
}
.flash {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  background: var(--surface-overlay); border: 1px solid var(--line-strong);
  border-radius: var(--r-3); padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--shadow-overlay);
  animation: flash-in var(--t-base) var(--ease-out);
}
@keyframes flash-in {
  from { opacity: 0; transform: translateX(8px); }
}
.flash--error { border-left: 2px solid var(--danger); }
.flash__body { flex: 1; min-width: 0; }
.flash__kind {
  display: block; font-family: var(--font-mono); font-size: 10px;
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--text-subtle); margin-bottom: 4px;
}
.flash--error .flash__kind { color: var(--danger-text); }
.flash__message { font-size: 13px; line-height: 1.5; color: var(--text); }
/* ── Shared canvas and welcome ─────────────────────────────────────────────
   Practice and Client use the same full-height canvas and front-door greeting.
   The host classes stay on the markup for local layout, while these classes
   own every declaration the two surfaces share. */

.surface-canvas {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  min-width: 0;
  padding: var(--sp-6) var(--sp-4);
  background: var(--surface);
}

/* The ruled waiting-room ground retired 2026-09-12 by the F1 proofs: both
   welcome canvases (Practice's front door, Client's home) render flat now,
   with nothing standing in for the old `--grid` substrate. `.surface-canvas`
   itself already carried no grid of its own, so there is nothing left to
   draw here; `SurfaceComponents.canvas/1` still normalizes a `:grid` variant
   to this same flat class until every caller is flat (see its module). */

/* Each front door centers its welcome group in the available canvas, with
   the same optical lift. Safe centering keeps short screens reachable. */
[data-welcome-stage] {
  align-items: center;
  justify-content: safe center;
  overflow-y: auto;
  padding: var(--sp-6) var(--sp-4)
    calc(var(--sp-6) + var(--safe-b) + clamp(48px, 12vh, 128px));
}

.surface-welcome {
  position: relative;
  width: min(100%, 720px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-8);
  padding: 0;
  margin: 0;
  text-align: center;
}

.surface-welcome__intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
}

/* The one serif role inside product (F2d wires the first consumer): a
   greeting or a welcome line, nothing dense, nothing tabular, never a
   control. Stays an h1; nothing else about the welcome block changes. */
.surface-welcome__greeting {
  font: var(--t-welcome);
  letter-spacing: var(--tr-welcome);
  font-optical-sizing: none;
  font-variation-settings: "opsz" 32;
  color: var(--text);
  margin: 0;
}

/* The old Address Exception (DESIGN.md 7c and 7d) is retired: a person's
   name is not evidenced agent action, so the Signal Rule leaves it neutral
   - it takes whatever colour the greeting itself is set to. */
.surface-welcome__name {
  color: inherit;
}

/* The counterpart mark leads the greeting, inline with the first word so
   it wraps with it. Sized in em against the welcome step, seated at cap
   height rather than the line's middle, one word space before the text.
   It inherits the greeting's colour: Ink on Paper, Paper on Ink. It is a
   .brand-arrival, so it draws itself in on arrival like the login mark;
   `overflow: visible` keeps the stroke's outer half from clipping at the
   viewBox edge while it draws. */
.surface-welcome__mark {
  width: 0.86em;
  height: 0.86em;
  vertical-align: -0.08em;
  margin-right: 0.36em;
  color: inherit;
  overflow: visible;
}

@media (max-width: 559.98px) {
  [data-welcome-stage] {
    padding-bottom: calc(var(--sp-6) + var(--safe-b));
  }

  .surface-welcome__greeting {
    font: var(--t-welcome-sm);
    letter-spacing: var(--tr-welcome);
    text-wrap: balance;
  }
}

/* Shared fields: one label/control/hint/error anatomy across console forms. */
/* One field: label (with its freeze marker), control, then hint or error. */
.onboard-field { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 4px var(--sp-4); align-items: baseline; padding: var(--sp-3) 0; border-bottom: 1px solid var(--surface-rule); }
.onboard-field:last-of-type { border-bottom: none; }
.onboard-field__label { display: inline-flex; align-items: center; gap: 6px; font: var(--t-body-sm); color: var(--text-muted); }
.onboard-field__lock { display: inline-flex; color: var(--text-subtle); }
.onboard-field__control { min-width: 0; }
.onboard-field__hint,
.onboard-field__error { grid-column: 2; font: var(--t-micro); margin: 0; }
.onboard-field__hint { color: var(--text-faint); }
.onboard-field__freeze { color: var(--text-subtle); }
.onboard-field__error { color: var(--danger-text); }
.onboard-field--error .onboard-input { border-color: var(--danger); }

/* Input aliases retain each caller's geometry and DOM contracts. These three
   controls share their appearance here; product.css owns width, inline flex,
   mono/textarea variants and the touch-size override. */
.reject-form__input,
.ack-form__input,
.onboard-input {
  padding: var(--field-input-padding, 7px 10px);
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-2);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 13px;
}

/* Human focus is neutral (DESIGN.md's Signal Rule): a person typing into a
   field is not evidenced agent action, so the old "sanctioned non-agent use
   of the signal" border is retired here - the same fix product.css's
   `.tb__filter` already carried for its sibling pattern. The keyboard ring
   stays --focus-ring and the border escalates to --line-strong, the same
   step `.onboard-input:hover` already uses. One rule for every field
   variant. */
.reject-form__input:focus-visible,
.ack-form__input:focus-visible,
.onboard-input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
  border-color: var(--line-strong);
}

/* Shared conversation frame (COT-449). Surface sheets keep measure, bubble
   fill and author size. Flex/gutter/min-width live here so a nested frame
   does not depend on product.css. */
.conv-frame { min-width: 0; }
.conv-frame--start { display: flex; align-items: flex-start; gap: 10px; }
.conv-frame--end { align-self: flex-end; }
.conv-answer { min-width: 0; }

/* ── Chat markdown: the answer as a ledger, not a wall of text ──
   Deterministic blocks from ChatMarkdown (COT-116: moved here from
   product.css so the Client host - which loads this sheet but not
   product.css - gets the neutral renderer for free, unchanged). Tables
   carry the data-table vocabulary at chat scale: mono headers, hairline
   rules, money columns right-aligned in mono with tabular numerals. Prose
   stays sans (numerals inside sentences keep the documented carve-out). */
.chat-md { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.chat-md__p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--text); overflow-wrap: anywhere; }
.chat-md__h { margin: 4px 0 0; font-family: var(--font-mono); font-weight: 500; letter-spacing: var(--tr-wide); text-transform: uppercase; color: var(--text-subtle); }
.chat-md__h--1 { font-size: 11px; }
.chat-md__h--2 { font-size: 11px; }
.chat-md__h--3 { font-size: 10px; }
.chat-md__list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; font-size: 14px; line-height: 1.55; color: var(--text); }
.chat-md__list li::marker { color: var(--text-faint); }
.chat-md__code { font-family: var(--font-mono); font-size: 0.9em; color: var(--text); background: var(--surface); border: 1px solid var(--surface-rule); border-radius: var(--r-1); padding: 0 4px; }
.chat-md__link { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
.chat-md__hr { border: none; border-top: 1px solid var(--surface-rule); margin: 4px 0; }
/* A quoted callout: the rule on the left carries it, the way every other
   emphasis in this system is carried by a border rather than a fill. */
.chat-md__quote { margin: 0; padding-left: 11px; border-left: 2px solid var(--surface-rule); font-size: 14px; line-height: 1.6; color: var(--text-muted); }
/* `min-width: 0`, the same floor `.table-scroll` and `.batch-review__scroll`
   carry (I6 §7.3): without it a flex or grid ancestor sizes this box to the
   TABLE's min-content width instead of to the column, the sideways scroller
   never engages, and the widest row pushes the document past the viewport -
   at 390 a chat holding a table shrank the layout viewport to 591px and the
   phone rendered the whole room zoomed out. */
.chat-md__scroll { overflow-x: auto; min-width: 0; border: 1px solid var(--surface-rule); border-radius: var(--r-2); }
.chat-md__table { width: 100%; border-collapse: collapse; font-size: 12px; }
.chat-md__table th { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--text-subtle); text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--surface-rule); background: var(--surface); white-space: nowrap; }
.chat-md__table td { padding: 6px 10px; color: var(--text); border-bottom: 1px solid var(--surface-rule); vertical-align: baseline; }
.chat-md__table tbody tr:last-child td { border-bottom: none; }
.chat-md__table .chat-md__num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Machine-value columns (dates, ranges, IDs): mono like every value in the
   system, but left-aligned - they are identifiers, not figures. */
.chat-md__table .chat-md__val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.chat-md__table td strong { font-weight: 600; }

/* The reveal island (assets/js/stream_reveal.mjs) mirrors the source
   .chat-md and paces its text out; empty until the hook mounts, so it
   collapses rather than leaving a gap before JS runs. */
.chat-md-reveal:empty { display: none; }
/* No JS (or a hook that has not mounted yet) leaves the island empty, and
   the server-rendered source is what shows - unchanged, exactly as it
   always has. Only once the mirror holds content does the source hide, so
   there is never a moment with both the raw source AND the paced mirror on
   screen at once. */
[data-stream-reveal]:has(> .chat-md-reveal:not(:empty)) > .chat-md { display: none; }

/* The growing edge of an answer in flight (COT-383). While the agent is
   answering, the words are the activity and this caret is its one cue: a
   thin signal bar after the last character, where the next one will land.
   It rides the block that holds the last text (a paragraph, the last list
   item, the last table cell) so it sits at the end of the sentence and not
   on a line of its own. `data-answering` is written by the answer's body
   only in the :answering phase; a tool round drops it and the busy row
   speaks instead, so there is never a caret AND a status line. Signal is
   sanctioned here for the same reason it is on the orb: the caret exists
   only while a run streams over the socket. The reveal hook (commit 4)
   marks the frontier block `data-frontier` while it is still pacing text
   out; then the frontier carries the caret and the sheet's last-child
   fallback stands down. */
[data-answering] .chat-md > .chat-md__p:last-child::after,
[data-answering] .chat-md > .chat-md__h:last-child::after,
[data-answering] .chat-md > .chat-md__quote:last-child::after,
[data-answering] .chat-md > .chat-md__list:last-child > li:last-child::after,
[data-answering] .chat-md > .chat-md__scroll:last-child tr:last-child > :last-child::after,
[data-answering] .chat-md [data-frontier]::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1em;
  /* Zero advance: the bar hangs into the margin instead of taking a slot in
     the line, so a word that ends flush at the measure never wraps the
     caret alone onto a new line and back as the next word lands. */
  margin-left: 2px;
  margin-right: -4px;
  vertical-align: -0.15em;
  border-radius: var(--r-1);
  background: var(--signal);
  animation: chat-caret 1s steps(2, jump-none) infinite;
}
/* `:not([data-frontier])` because these selectors outrank the frontier rule
   above: while the final item or cell is itself the frontier it must keep
   its caret, not lose it to its own fallback (Astra's review). */
[data-answering] .chat-md--paced > :last-child:not([data-frontier])::after,
[data-answering] .chat-md--paced > .chat-md__list:last-child > li:last-child:not([data-frontier])::after,
[data-answering] .chat-md--paced > .chat-md__scroll:last-child tr:last-child > :last-child:not([data-frontier])::after {
  content: none;
}
/* Text is still flowing under the caret: it holds steady rather than blinks.
   A blink says "waiting for the next word"; a bar that moves says "writing". */
[data-answering] .chat-md--paced [data-frontier]::after { animation: none; }
@keyframes chat-caret { from { opacity: 1; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  [data-answering] .chat-md [data-frontier]::after,
  [data-answering] .chat-md > :last-child::after,
  [data-answering] .chat-md > .chat-md__list:last-child > li:last-child::after,
  [data-answering] .chat-md > .chat-md__scroll:last-child tr:last-child > :last-child::after { animation: none; }
}

/* ── Shared composer ───────────────────────────────────────────────────────
   One form and one browser contract serve Console, Practice, and Client.
   Surface sheets decide where the form docks and how wide it is; this neutral
   layer owns the writing surface, its primary controls, responsive anatomy,
   and the live edge so a host never has to recreate the component. */
.composer,
.composer__footer {
  display: contents;
}

/* The one quiet door beneath a welcome composer (composer_hint/1): Console's
   "Command + K to search", Client's "+ Connect your favourite AI". Centred,
   alone and 44px tall, so it takes the standalone-link convention (underline
   on hover and focus, not at rest) rather than base.css's prose underline.
   Muted at rest so the field above keeps the eye, --text when reached for.
   Body type, never italic: Inter Tight has no italic role in DESIGN.md, and
   the coarse-pointer step lifts it to 16px with the rest of the body copy.
   Link and button share the rule; only the host's destination differs. The
   words travel in one .composer__hint-label so a live shortcut span and its
   trailing text stay one flex item with their space intact. */
.composer__hint {
  display: flex;
  align-items: center;
  justify-content: center;
  width: fit-content;
  min-height: var(--tap-min);
  margin: var(--sp-2) auto 0;
  padding: 0 var(--sp-3);
  border: 0;
  background: none;
  font: var(--t-body);
  color: var(--text-muted);
  text-decoration: none;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color var(--t-quick) var(--easing);
}

.composer__hint:hover,
.composer__hint:focus-visible {
  color: var(--text);
  text-decoration: underline;
}

.composer__form { position: relative; }

/* The file picker shares the composer's type and phone input floor even
   when the host loads no staff stylesheet. */
.composer input[type="file"] {
  font-family: var(--font-sans);
  font-size: 16px;
}

.composer__actions {
  display: contents;
}

.chat__input {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-3);
  background: var(--surface-card);
  transition: border-color var(--t-base) var(--easing);
}

.chat__input:focus-within {
  border-color: var(--signal);
}

.chat__input.phx-drop-target-active {
  border-color: var(--signal);
  border-style: dashed;
}

.chat__field {
  flex: 1;
  min-width: 0;
  min-height: 1.5em;
  max-height: 160px;
  padding: 2px 0;
  border: 0;
  outline: none;
  background: none;
  color: var(--text);
  font: 400 14px/1.5 var(--font-sans);
  resize: none;
}

.chat__field::placeholder {
  color: var(--text-faint);
}

.phero__field::placeholder { color: var(--text-subtle); }

.chat__send,
.chat__attach,
.chat__mic-btn {
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: var(--r-2);
}

.chat__send,
.chat__mic-btn {
  display: grid;
  place-items: center;
  border: 1px solid var(--surface-rule);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  transition:
    color var(--t-base) var(--easing),
    border-color var(--t-base) var(--easing),
    background-color var(--t-base) var(--easing),
    transform var(--t-quick) var(--ease-out);
}

.chat__send:hover:not(:disabled),
.chat__mic-btn:hover:not(:disabled) {
  border-color: var(--text-faint);
  color: var(--text);
}

.chat__send[data-ready] {
  border-color: var(--signal);
  background: var(--signal);
  color: var(--signal-ink);
}

.chat__send[data-ready]:hover:not(:disabled) {
  border-color: var(--signal-soft);
  background: var(--signal-soft);
  color: var(--signal-ink);
}

.chat__send:disabled {
  opacity: 0.45;
  cursor: default;
}

.chat__send .icon { transform: rotate(-90deg); }

.chat__send:active:not(:disabled),
.chat__mic-btn:active:not(:disabled) {
  transform: scale(0.94);
}

.composer__stop {
  margin-left: 6px;
  padding: 4px 10px;
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-2);
  background: none;
  color: var(--text-muted);
  /* A control label, so the interface face: mono is for values (DESIGN.md). */
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition:
    color var(--t-quick) var(--easing),
    border-color var(--t-quick) var(--easing);
}

.composer__stop:hover {
  border-color: var(--text-faint);
  color: var(--text);
}

@media (pointer: coarse) {
  .composer__stop {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap-dense);
  }
}

.chat__attach {
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  background: none;
  color: var(--text-faint);
  cursor: pointer;
  transition:
    color var(--t-base) var(--easing),
    border-color var(--t-base) var(--easing);
}

.chat__attach:hover {
  border-color: var(--surface-rule);
  color: var(--text);
}

.chat-attach-tray {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 0 0 8px;
}

.chat-file-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-2);
  background: var(--surface-elev);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
}

.chat-file-chip__thumb {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 3px;
  object-fit: cover;
}

.chat-file-chip .icon { flex: none; color: var(--text-faint); }

.chat-file-chip__name {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-file-chip__meta,
.chat-file-chip__x { color: var(--text-faint); }

.chat-file-chip__x {
  display: inline-flex;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transition: color var(--t-quick) var(--easing);
}

.chat-file-chip__x:hover { color: var(--text); }
.chat-file-chip__x:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

.chat-attach-note {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
}

.chat__mic {
  position: relative;
  display: flex;
  flex: none;
}

.chat__mic-btn {
  --level: 0;
  position: relative;
  padding: 0;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.chat__mic-btn:disabled { cursor: default; }

.chat__mic-glyph {
  grid-area: 1 / 1;
  display: grid;
  place-items: center;
  opacity: 0;
  transform: scale(0.7);
  transition:
    opacity var(--t-base) var(--easing),
    transform var(--t-base) var(--ease-out);
}

.chat__mic-glyph--rec {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: currentColor;
}

.chat__mic-btn[data-state="idle"] .chat__mic-glyph--idle,
.chat__mic-btn[data-state="starting"] .chat__mic-glyph--idle,
.chat__mic-btn[data-state="recording"] .chat__mic-glyph--rec,
.chat__mic-btn[data-state="done"] .chat__mic-glyph--done {
  opacity: 1;
  transform: scale(1);
}

.chat__mic-level {
  grid-area: 1 / 1;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--danger);
  opacity: 0;
  transform: scale(0.5);
  pointer-events: none;
}

.chat__mic-btn[data-state="recording"] {
  border-color: var(--danger);
  color: var(--danger);
}

.chat__mic-btn[data-state="recording"] .chat__mic-level {
  opacity: calc(0.1 + var(--level) * 0.3);
  transform: scale(calc(0.5 + var(--level) * 0.5));
}

.chat__mic-orb {
  grid-area: 1 / 1;
  width: 18px;
  height: 18px;
  opacity: 0;
  transition: opacity var(--t-quick) var(--easing);
  pointer-events: none;
}

.chat__mic-btn[data-state="finishing"] { color: transparent; }
.chat__mic-btn[data-state="finishing"] .chat__mic-orb { opacity: 1; }
.chat__mic-btn[data-state="starting"] { color: var(--text-faint); }
.chat__mic-btn[data-state="done"] { color: var(--success); }
.chat__mic-btn[data-state="error"] { border-color: var(--danger); color: var(--danger); }

.chat__mic-timer {
  position: absolute;
  top: 50%;
  right: calc(100% + 10px);
  z-index: var(--z-tip);
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: 0.06em;
  transform: translateY(-50%);
  animation: composer-mic-slide var(--t-base) var(--ease-out);
}

.chat__mic-timer[data-warn] { color: var(--danger-text); }
.chat__mic-timer[hidden],
.chat__mic-note[hidden] { display: none; }

.chat__mic-note {
  position: absolute;
  right: 0;
  bottom: calc(100% + 8px);
  z-index: var(--z-tip);
  width: max-content;
  max-width: 240px;
  padding: 6px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-2);
  background: var(--surface-overlay);
  box-shadow: var(--shadow-overlay);
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.4;
  animation: composer-mic-rise var(--t-base) var(--ease-out);
}

.chat__mic-discard {
  min-height: var(--tap-dense);
  padding: 0 var(--sp-2);
  border: 0;
  background: none;
  color: var(--text-subtle);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: var(--tr-wide);
  cursor: pointer;
  transition: color var(--t-quick) var(--easing);
}

.chat__mic-discard:hover { color: var(--danger-text); }
.chat__mic-discard:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

@keyframes composer-mic-rise {
  from { opacity: 0; transform: translateY(4px); }
}

@keyframes composer-mic-slide {
  from { opacity: 0; transform: translate(6px, -50%); }
}

/* Hero is the same component with more writing room and a travelling edge
   while the host reports an active run. Flat, ruled surfaces (the F1 proofs,
   COT-234a): no shadow, and the hairline is the stronger --line-strong edge
   a control draws around itself, not the page's own --surface-rule. */
.phero__box {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-3);
  background: var(--surface-card);
  transition: border-color var(--pt-fast) linear;
}

.phero__box:focus-within {
  border-color: var(--signal);
}

.phero__box.phx-drop-target-active {
  border-color: var(--signal);
  border-style: dashed;
}

.phero__box[data-live]:focus-within {
  border-color: var(--line-strong);
}

.phero__field {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  min-height: var(--control-h);
  padding: var(--sp-1) 0;
  border: 0;
  outline: none;
  background: none;
  color: var(--text);
  font: var(--t-body-touch);
  overflow-y: auto;
  resize: none;
}

.composer--hero .composer__actions {
  display: inline-flex;
  align-items: center;
  align-self: flex-end;
  gap: var(--sp-2);
  flex: none;
}

.phero__box > .chat__attach { align-self: flex-end; }

.composer--hero .chat__mic {
  align-items: center;
  gap: var(--sp-2);
}

.composer--hero .chat__mic-timer {
  position: static;
  transform: none;
  animation: none;
}

.phero__live {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.phero__live rect {
  x: 0.5px;
  y: 0.5px;
  width: calc(100% - 1px);
  height: calc(100% - 1px);
  rx: var(--r-4);
  fill: none;
  stroke: var(--signal);
  stroke-width: 1;
  animation: 2s linear infinite;
}

.phero__live rect:nth-child(1) {
  stroke-dasharray: 44 956;
  animation-name: composer-live-a;
}

.phero__live rect:nth-child(2) {
  stroke-dasharray: 76 924;
  stroke-opacity: 0.4;
  animation-name: composer-live-b;
}

.phero__live rect:nth-child(3) {
  stroke-dasharray: 108 892;
  stroke-opacity: 0.16;
  animation-name: composer-live-c;
}

@keyframes composer-live-a {
  from { stroke-dashoffset: 22; }
  to { stroke-dashoffset: -978; }
}

@keyframes composer-live-b {
  from { stroke-dashoffset: 38; }
  to { stroke-dashoffset: -962; }
}

@keyframes composer-live-c {
  from { stroke-dashoffset: 54; }
  to { stroke-dashoffset: -946; }
}

@media (max-width: 559.98px) {
  .composer--hero .phero__box {
    flex-wrap: wrap;
    padding: var(--sp-3) var(--sp-4) var(--sp-2);
  }

  .composer--hero .phero__field {
    order: -1;
    flex: 1 0 100%;
    padding-bottom: var(--sp-2);
  }

  .composer--hero .phero__box > .chat__attach {
    margin-right: auto;
  }
}

/* One welcome composer across Console, Practice and Client. The field has
   two lines of writing room before it grows, with controls along the floor.
   Surface sheets own placement only; fill and geometry live here. */
.composer--welcome .phero__box {
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
}

.composer--welcome .phero__field {
  order: -1;
  flex: 1 0 100%;
  min-height: calc(2 * 1.55em);
  font: var(--t-compose);
}

.composer--welcome .composer__actions {
  margin-left: auto;
}

@media (pointer: coarse) {
  .composer .chat__field {
    min-height: var(--tap-dense);
    padding: 7px 0;
    font: var(--t-body-touch);
  }

  .composer .chat__send,
  .composer .chat__mic-btn,
  .composer .chat__attach {
    width: var(--tap-min);
    height: var(--tap-min);
  }

  .composer .chat__mic-discard {
    min-height: var(--tap-min);
    padding: 0 var(--sp-3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .composer .chat__send:active:not(:disabled),
  .composer .chat__mic-btn:active:not(:disabled) {
    transform: none;
  }

  .composer .chat__mic-glyph {
    transform: none;
    transition: opacity var(--t-quick) var(--easing);
  }

  .composer .chat__mic-btn[data-state="recording"] .chat__mic-level {
    opacity: 0.22;
    transform: scale(0.8);
  }

  .composer .chat__mic-timer,
  .composer .chat__mic-note { animation: none; }

  .phero__live rect { animation: none; }
  .phero__live rect:nth-child(1) { stroke-dashoffset: -718; }
  .phero__live rect:nth-child(2) { stroke-dashoffset: -702; }
  .phero__live rect:nth-child(3) { stroke-dashoffset: -686; }
}

/* ── The navigation column, shared ─────────────────────────────────────────
   `CotrugliWeb.ShellComponents.shell_drawer/1`: the one column Console,
   Practice, and Client wear. It lived in room.css until Practice was the
   only host that had it, then in product.css while only the staff hosts wore
   it. It lives here now because components.css is the neutral sheet all three
   authenticated hosts load, and a column edited per host is several columns.

   What is here is the FRAME: the column and its collapse, the brand row, the
   scrolling body, and the account foot. What each host hangs in that frame is
   its own - Practice's roster rows and their vocabulary (`.proster*`,
   `.pdrow*`, `.pmono*`) stay in room.css, Console destinations stay in
   product.css, and Client conversations stay in client.css. A row is content;
   this is furniture.

   Nothing here wears --signal. Practice's waiting-count badge stays in its
   own sheet because it is roster state, not shared chrome. */

/* The column's motion values - --pt-fast, --pt-move, --pt-ease - are in
   tokens.css with the rest of the ramp, not here. They were room.css's
   for as long as the column was Practice's; a value three hosts depend on has
   no business in a layer sheet, and tokens.css is the ONE token sheet. */

/* The scrim has no box until the shared column becomes a phone overlay. */
.pdrawer-scrim { display: none; }

/* One external opener on every surface. Practice adds its needs-count badge
   and pulse in room.css; Client supplies no status content. Console seats
   the same control inline in its topbar. Placement never changes its shape. */
.pdrawer-toggle {
  position: fixed;
  top: var(--sp-4);
  left: var(--sp-4);
  z-index: var(--z-nav);
  display: grid;
  place-items: center;
  width: var(--tap-min, 44px);
  height: var(--tap-min, 44px);
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-4);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Hidden while the drawer is open, then removed from the tab order once
     the fade finishes. The closed rule shows it with no delay. */
  transition: color var(--pt-fast) var(--pt-ease), border-color var(--pt-fast) var(--pt-ease),
    opacity var(--pt-fast) linear, visibility 0s linear var(--pt-fast);
}

.pdrawer-toggle--inline {
  position: static;
  flex: none;
  top: auto;
  left: auto;
  z-index: auto;
}

html[data-pdrawer="closed"] .pdrawer-toggle {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: color var(--pt-fast) var(--pt-ease), border-color var(--pt-fast) var(--pt-ease),
    opacity var(--pt-fast) linear, visibility 0s linear;
}

.pdrawer-toggle:hover {
  color: var(--text);
  border-color: var(--line-strong, var(--text-faint));
}

.pdrawer {
  /* COT-574: `--pdrawer-w` is the drag handle's own variable, set on
     `<html>` by `.PanelResize` (or, before its hook ever mounts, by the
     pre-paint script in `ds_root.html.heex`) - the fallback is this rule's
     old flat 300px, unchanged for every page nothing has ever dragged. */
  width: var(--pdrawer-w, 300px);
  min-width: var(--pdrawer-w, 300px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  /* No bottom padding of its own: the account corner is the column's foot
     and carries the foot's air itself, so its hairline lands on the same
     line as the reply box's (the owner, 2026-09-13). */
  padding: var(--sp-4) var(--sp-3) 0;
  background: var(--surface);
  border-right: 1px solid var(--surface-rule);
  overflow: hidden;
  visibility: visible;
  /* Stays in the viewport while the client canvas is the document that
     scrolls (Claude/ChatGPT: one page scroll, drawer as furniture).
     Phone overlay below `md` re-declares `position: fixed` and wins. */
  position: sticky;
  top: 0;
  align-self: start;
  /* The SAME height its shell has, `--kb-inset` included. `.app` is
     `calc(100dvh - var(--kb-inset, 0px))` with `overflow: hidden` (top of this
     sheet), and a raised software keyboard shrinks neither dvh nor the layout
     viewport - so a flat 100dvh here overruns the shell by the keyboard's
     height and the account foot is clipped off the bottom. The `.sidebar` this
     column replaced never had the bug: it was a stretched flex item and got
     `.app`'s height for free. The account MENU already reserves for the
     keyboard (`--pdaccount-reserve`); the box it opens inside has to agree, or
     it is measuring room that is off screen. */
  height: calc(100dvh - var(--kb-inset, 0px));
  /* Width + opacity, the product ramp: expo-out on the move, linear on the
     fade - a collapse should look like the column giving up its space, not
     like the content behind it flickering. `visibility` rides along
     (opacity: 0 alone still leaves every control tabbable and in the AT
     tree - a collapsed drawer's rows must actually leave both, or a
     keyboard pass lands on invisible links and a screen reader hears two
     live needs-you counts at once, §7c's exclusivity broken in the one
     modality opacity can't cover). Shown with no delay (visibility 0s) so
     opening never lags the fade in; hidden only once the collapse motion
     finishes - the closed rule below carries that delay. */
  transition:
    width var(--pt-move) var(--pt-ease),
    min-width var(--pt-move) var(--pt-ease),
    padding var(--pt-move) var(--pt-ease),
    border-color var(--pt-move) var(--pt-ease),
    opacity var(--pt-fast) linear,
    visibility 0s linear;
}

html[data-pdrawer="closed"] .pdrawer {
  width: 0;
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
  opacity: 0;
  visibility: hidden;
  border-right-color: transparent;
  pointer-events: none;
  /* Same list as the base rule, visibility's delay swapped in: this is the
     state the browser reads when CLOSING, so the delay that matters lives
     here, not on the open rule above. */
  transition:
    width var(--pt-move) var(--pt-ease),
    min-width var(--pt-move) var(--pt-ease),
    padding var(--pt-move) var(--pt-ease),
    border-color var(--pt-move) var(--pt-ease),
    opacity var(--pt-fast) linear,
    visibility 0s linear var(--pt-move);
}

/* ─── The drawer's resize handle (COT-574) ───
   A thin separator on the column's INNER edge (its right side, the one
   facing the conversation), `position: absolute` against `.pdrawer`'s own
   `position: sticky` (which is itself a positioning context - no extra
   wrapper needed). An 8px hit area, wide enough for a pointer or a finger,
   holding a 1px hairline centred inside it. TRANSPARENT at rest (review
   finding): `.pdrawer` already draws its own `border-right: 1px solid
   var(--surface-rule)` at this exact edge, so a resting hairline here was a
   second rule 4px inboard of the panel's own, reading as two parallel
   lines rather than one. Only the panel's existing border shows at rest;
   hover, focus AND active brighten this one to `--line-strong` over
   `--t-quick` linear, the house's own press-feedback ramp - never a new
   colour. Kept fully INSIDE the box: `.pdrawer` is `overflow: hidden`
   (above), so anything hanging past `right: 0` would be clipped rather
   than grabbable. `.PanelResize` (shell_components.ex) answers
   `pointerdown`/keyboard on it and owns `--pdrawer-w`; this rule only ever
   draws it. */
.pdrawer-resize {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: col-resize;
  touch-action: none;
  background: none;
  border: 0;
  padding: 0;
  z-index: 1;
}

.pdrawer-resize::before {
  content: "";
  width: 1px;
  height: 100%;
  background: transparent;
  transition: background-color var(--t-quick) linear;
}

.pdrawer-resize:hover::before,
.pdrawer-resize:focus-visible::before,
.pdrawer-resize:active::before {
  background: var(--line-strong);
}

.pdrawer-resize:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

/* Dragging disables the column's own width/min-width transition (both
   panels', by the shared selector below) so the edge tracks the pointer
   with no lag - `data-resizing` lives on `<html>`, the SAME root the
   handle writes `--pdrawer-w`/`--canvas-w-open` to, set for the drag's
   duration by `.PanelResize` and nothing else. Higher specificity than
   either the open or the closed `.pdrawer` rule above (an extra type
   selector), so it wins regardless of source order. */
html[data-resizing] .pdrawer {
  transition: none;
}

/* Hidden, and its width ignored, below the drawer's own overlay
   breakpoint (DESIGN.md 6, `components.css`'s 900px phone rule further
   down): a phone drawer is a full-screen overlay with nothing beside it to
   trade width with, and `.pdrawer`'s own phone rule already hardcodes
   `min(300px, 85vw)` at higher specificity than `--pdrawer-w`, so the
   variable is simply never read there. Hiding the handle too keeps a
   coarse pointer from landing on an 8px target that does nothing useful
   over a slide-out panel. */
@media (max-width: 900px) {
  .pdrawer-resize {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pdrawer-resize::before {
    transition: none;
  }
}

/* The row is the lockup's own height on every host, and that is the whole of
   it: no pinned band, no host measurement, no inline custom property. It
   carried one until 2026-08-30 - a min-height off a property the console set
   inline from its own top bar's token - and the cost was the drift the shared
   column exists to prevent: a 0-56 band on the console against a 16-56 one on
   Practice, the same lockup 8px higher on one host. The console's top bar is
   what agrees with this row now, not the other way round. */
.pdrawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 0 var(--sp-1);
}

/* The console's own lockup (`brand/1`, `CotrugliWeb.CoreComponents`), in the
   drawer's header now that the top bar is gone from every screen: one brand
   on every host. It wears the lockup's own step because the room's
   identity bar stands beside it with a 40px client tile and an h1 name, and
   at a smaller step the brand read as the smaller of two marks on one line
   (the owner, 2026-08-29) - smaller, even, than the 28px tiles on the rows
   under it. 40 tall, not the tap minimum: the drawer opens with --sp-4 over it and
   the room bar opens with --sp-4 over its 40px tile, so the brand's centre
   and the client's tile's centre are the same 36px from the top - one line
   across the page. A coarse pointer gets the tap minimum back; there the
   drawer is an overlay and shares no line with the bar. */
.pdrawer__lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 40px;
  text-decoration: none;
  color: var(--text);
}

@media (pointer: coarse) {
  .pdrawer__lockup {
    min-height: var(--tap-min, 44px);
  }
}

.pdrawer__collapse {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  color: var(--text-muted);
  background: none;
  border: 0;
  border-radius: var(--r-3);
  cursor: pointer;
  transition: color var(--pt-fast) var(--pt-ease), background-color var(--pt-fast) var(--pt-ease);
}

.pdrawer__collapse:hover {
  color: var(--text);
  background: var(--surface-hover);
}

@media (pointer: coarse) {
  .pdrawer__collapse {
    width: var(--tap-min, 44px);
    height: var(--tap-min, 44px);
  }
}

.pdrawer__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ── The drawer's list (COT-474) ──
   One vocabulary for what hangs IN the column, modelled on Practice's roster
   (its section head; its row of mark, name, line and a mono value on the
   right rail; its three you-are-here marks). Console hangs destinations in
   it and Client hangs conversations; Practice's own roster keeps
   `.proster*`/`.pdrow*` in room.css until its migration card. A slot a
   host cannot fill honestly stays empty and the grid closes around it:
   `.pdlist__row` is two columns, `--marked` adds the 28px mark column. */
.pdlist {
  list-style: none;
  margin: 0;
  padding: 0;
}

.pdlist__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: 28px;
  margin: var(--sp-4) 0 var(--sp-1);
  padding: 0 var(--sp-3);
  font: inherit;
}

/* The drawer's own gap already separates the header from the body. */
.pdrawer__body > .pdlist__head:first-child,
.pdrawer__body > .pdlist:first-child {
  margin-top: var(--sp-1);
}

.pdlist__label {
  font: var(--t-mono-label);
  font-family: var(--font-mono);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--text-subtle);
}

/* A tally of rows, in faint ink: never the agent's colour (COT-234a). */
.pdlist__count {
  font: var(--t-mono-data);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

/* A folding head hands its box to the button, so the hover wash and the tap
   target are the whole head. */
.pdlist__head--fold {
  padding: 0;
}

.pdlist__fold {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: 28px;
  padding: 0 var(--sp-3);
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--r-3);
  cursor: pointer;
}

.pdlist__fold:hover {
  background: var(--surface-hover);
}

.pdlist__fold:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

.pdlist__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.pdlist__caret {
  color: var(--text-faint);
  transition: transform var(--pt-fast) var(--pt-ease);
}

.pdlist__fold[aria-expanded="true"] .pdlist__caret {
  transform: rotate(90deg);
}

/* The item is the row's seat: the link fills it and a host's one act (Client's
   archive) hangs on its right edge outside the link, because a button inside
   an anchor is not markup. */
.pdlist__item {
  position: relative;
}

.pdlist__row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  min-height: var(--tap-min, 44px);
  color: inherit;
  text-decoration: none;
  border-radius: var(--r-3);
  transition: background-color var(--pt-fast) var(--pt-ease);
}

.pdlist__row--marked {
  grid-template-columns: auto 1fr auto;
}

.pdlist__row:hover {
  background: var(--surface-hover);
}

.pdlist__row:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

/* You-are-here: Practice's three marks and none of them colour. The elevated
   fill, a 2px ink rail inset from the row's ends (a positioned element, never
   an inset shadow: a shadow follows the radius and curls into a bracket), and
   the name at weight. Picking a row is a human act; the Signal Rule keeps the
   agent's colour for the moments an agent acted. */
.pdlist__row.is-current {
  background: var(--surface-elev);
}

.pdlist__row.is-current:hover {
  background: var(--surface-active);
}

.pdlist__row.is-current::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--sp-1);
  bottom: var(--sp-1);
  width: 2px;
  border-radius: 999px;
  background: var(--text);
}

.pdlist__row.is-current .pdlist__name {
  font-weight: 600;
  color: var(--text);
}

/* The mark column: 28px, the roster tile's width, so every name across the
   three hosts shares one left edge. An icon whispers a step under its label
   and speaks in full ink on the current row. */
.pdlist__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  color: var(--text-subtle);
  transition: color var(--pt-fast) var(--pt-ease);
}

.pdlist__row:hover .pdlist__mark,
.pdlist__row.is-current .pdlist__mark {
  color: var(--text);
}

.pdlist__main {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.pdlist__name {
  font: var(--t-body-sm);
  font-weight: 500;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pdlist__line {
  font: var(--t-micro);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pdlist__value {
  font: var(--t-mono-data);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}

/* The act takes the value's seat while the pointer or focus is on the row and
   gives it back after. A coarse pointer has no hover, so there the act is
   always present and the value steps aside for good. */
.pdlist__act {
  position: absolute;
  right: var(--sp-2);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--pt-fast) linear;
}

.pdlist__item:hover .pdlist__act,
.pdlist__item:focus-within .pdlist__act {
  opacity: 1;
  pointer-events: auto;
}

.pdlist__item:has(.pdlist__act):hover .pdlist__value,
.pdlist__item:has(.pdlist__act):focus-within .pdlist__value {
  visibility: hidden;
}

@media (pointer: coarse) {
  .pdlist__act {
    opacity: 1;
    pointer-events: auto;
  }

  .pdlist__item:has(.pdlist__act) .pdlist__value {
    visibility: hidden;
  }
}

/* Press feedback and hover suppression on touch, the same doctrine product.css
   applies to its own rows: a tap acknowledges with the surface step; a hover
   wash that lingers after a tap reads as a selection. */
@media (hover: none) {
  .pdlist__row:hover {
    background: transparent;
  }

  .pdlist__row.is-current:hover {
    background: var(--surface-elev);
  }

  .pdlist__row:active {
    background: var(--surface-active);
  }
}

/* The gutter's one button shape (Practice's "+ New"; Client's "+ New"). Moved
   from room.css (COT-474) so the Client host, which loads no room
   sheet, can wear it. Declarations unchanged. */
.pdrawer__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min, 44px);
  padding: 0 var(--sp-3);
  font: var(--t-body-sm);
  color: var(--text);
  text-decoration: none;
  background: var(--surface-elev);
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-3);
  transition: border-color var(--pt-fast) var(--pt-ease);
}

.pdrawer__cta:hover {
  border-color: var(--line-strong, var(--text-faint));
}

/* The menu's own rise, declared beside its first caller (`.pdaccount__menu`,
   below). Keyframes are document-global, so room.css's filter popover and
   agent menu still animate by this one name. */
@keyframes pmenu-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

/* ─── The account corner ─── */

.pdaccount {
  position: relative;
  margin-top: auto;
  /* The shell's ONE foot rhythm (the owner, 2026-09-13): the drawer's foot
     and the room's reply box are the two feet of one screen, and their
     hairlines sat 12px apart because each was built from its own air. This
     corner now takes the composer anchor's anatomy exactly - a hairline,
     --sp-3 above, a 50px control, max(--sp-4, --safe-b) below - so the two
     rules land on one line and both columns end together. */
  padding-top: var(--sp-3);
  padding-bottom: max(var(--sp-4), var(--safe-b));
  border-top: 1px solid var(--surface-rule);
  /* Everything between the menu's bottom edge and the bottom of the screen,
     as one number the menu's own `max-height` subtracts: the column's bottom
     padding, this corner's own box (its top padding, its hairline, the
     trigger, and whatever the notch asks for on a phone), the --sp-2 the menu
     floats above the trigger by, and the keyboard when one is up (--kb-inset
     is 0 unless the ViewportInset hook has measured one). Written here, on
     the box those parts belong to, so a change to the foot's padding is a
     change to this in the same place. The trigger is BUDGETED at the tap
     floor plus a step, not at it: its own content (a 30px face on --sp-2
     padding) runs a couple of pixels over 44, and this number erring small
     means the menu scrolls a little sooner, where erring large means a row
     silently disappears. */
  --pdaccount-reserve: calc(
    var(--sp-3) + 1px + 50px + max(var(--sp-4), var(--safe-b)) + var(--kb-inset, 0px) +
      var(--sp-2)
  );
}

.pdaccount__trigger {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  /* The reply box's own vertical air (10px, .chat__input) around the 30px
     face: 50px, the box's resting height, so the shell's two feet match.
     Over the tap floor, not at it. */
  min-height: 50px;
  padding: 10px var(--sp-1);
  background: none;
  border: 0;
  border-radius: var(--r-3);
  cursor: pointer;
  text-align: left;
  transition: background-color var(--pt-fast) var(--pt-ease);
}

.pdaccount__trigger:hover,
.pdaccount__trigger[aria-expanded="true"] {
  background: var(--surface-hover);
}

.pdaccount__avatar {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  font: var(--t-mono-label);
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  color: var(--text-muted);
  background: var(--surface-elev);
  border: 1px solid var(--surface-rule);
  border-radius: 999px;
}

/* The same 30px disc with a face in it: the monogram's grid centring has
   nothing left to centre, and a photo that is not square is cropped to the
   tile rather than squashed into it. */
.pdaccount__avatar--img {
  display: block;
  object-fit: cover;
  padding: 0;
}

.pdaccount__email {
  font: var(--t-body-sm);
  color: var(--text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pdaccount__menu {
  position: absolute;
  left: var(--sp-1);
  right: var(--sp-1);
  bottom: calc(100% + var(--sp-2));
  z-index: var(--z-menu);
  background: var(--surface-overlay, var(--surface-elev));
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-4);
  box-shadow: var(--shadow-overlay);
  padding: var(--sp-2);
  display: flex;
  flex-direction: column;
  animation: pmenu-in var(--pt-fast) var(--pt-ease);
  /* The menu opens UPWARD inside a column that is `overflow: hidden` and
     exactly `100dvh` tall, which is what let the portal go (2026-08-30) - and
     which means a menu taller than the space above this foot is not
     "overflowing", it is silently GONE from the top down. It opens upward and
     DOM order runs Theme, Profile, the host's doors, Log out, so the row that
     disappears first is Theme and the row that survives longest is Log out:
     the failure is quiet at both ends and there is no scrollbar to say it
     happened. Five rows at the 44px floor is ~236px, which is comfortable on
     a laptop and is NOT on a landscape phone or under an open keyboard.
     So the ceiling is the space that actually exists, and past it the menu
     scrolls: no row can become unreachable. `overscroll-behavior` keeps that
     scroll off the page behind it. */
  max-height: calc(100dvh - var(--pdaccount-reserve));
  overflow-y: auto;
  overscroll-behavior: contain;
}

.pdaccount__menu[hidden] {
  display: none;
}

.pdaccount__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  font: var(--t-body-sm);
  color: var(--text);
  text-decoration: none;
  padding: var(--sp-3);
  min-height: var(--tap-min, 44px);
  border-radius: var(--r-3);
}

.pdaccount__item:hover {
  background: var(--surface-hover);
}

/* A hop row's label GROUP (`ShellComponents.hop/1`): the label and whatever
   qualifies it - the client the console's Open Practice will land on. One
   flex item, so the row stays the two the rest of this menu is (`Theme` and
   its state, a label and its arrow) and `space-between` keeps the arrow on
   the right edge rather than centring a client's name in the middle of the
   row. `min-width: 0` so a long name shrinks the group instead of widening
   the menu. */
.pdaccount__hop {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* The theme row is a <button> among <a>s: strip the user agent's box so the
   three rows read as one list. Font and colour are restated rather than
   inherited, because a button inherits neither by default. */
button.pdaccount__item {
  appearance: none;
  width: 100%;
  margin: 0;
  border: 0;
  background: none;
  font: var(--t-body-sm);
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

button.pdaccount__item:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.pdaccount__item--exit {
  color: var(--text-muted);
}

/* The theme item's current-value tail. It lives HERE, in the drawer's own
   section, because `shell_account/1` is the live component that renders
   it. The `ptop__` prefix is a fossil of the top bar the drawer replaced;
   the name is all that is left of it. */
.ptop__menu-theme {
  display: inline-flex;
  align-items: center;
  font: var(--t-mono-data);
  color: var(--text-subtle);
}

/* Which of the three words is the tail, and it is keyed on the PREFERENCE
   (`html[data-theme-pref]`, written pre-paint by ds_root's bootstrap and
   rewritten by ds/theme.js on click) rather than on the painted theme. That
   is the whole reason the row can say System at all: System and Dark paint
   the same page on a dark machine and are not the same answer, so a rule
   reading `html.dark` could never tell them apart. No attribute at all - a
   surface that somehow shipped without the bootstrap - reads as System,
   which is also what an unset key resolves to. */
.theme-cycle__dark,
.theme-cycle__light,
.theme-cycle__system {
  display: none;
  align-items: center;
  gap: var(--sp-2);
}

html[data-theme-pref="dark"] .theme-cycle__dark,
html[data-theme-pref="light"] .theme-cycle__light,
html[data-theme-pref="system"] .theme-cycle__system,
html:not([data-theme-pref]) .theme-cycle__system {
  display: inline-flex;
}

/* Below md the drawer is a phone overlay, not a column (DESIGN.md 6): fixed
   full-height, over a scrim, `--z-sidebar` above `--z-scrim`. The pre-paint
   script (ds_root.html.heex) starts `data-pdrawer` "closed" at this step
   regardless of what a wider session left in storage, so a phone visit
   always meets the hero and the toggle's badge, never an inherited-open
   drawer; from there the SAME attribute opens and closes it on BOTH staff
   hosts (`.ShellDrawer`, colocated on the column in `ShellComponents`).
   Escape, a scrim tap, a row's own navigate, the `inert` cover over
   everything beside the column and the body scroll lock all route through
   that one hook - the lock because neither `inert` nor a fixed scrim stops a
   drag from chaining to the scroller underneath. */
@media (max-width: 900px) {
  .pdrawer-scrim {
    display: block; position: fixed; inset: 0; z-index: var(--z-scrim);
    background: var(--scrim); opacity: 0; pointer-events: none;
    transition: opacity var(--t-slow) var(--easing);
  }

  html[data-pdrawer="open"] .pdrawer-scrim { opacity: 1; pointer-events: auto; }

  .pdrawer {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--z-sidebar);
    width: min(300px, 85vw);
    min-width: 0;
    opacity: 1;
    pointer-events: auto;
    transform: translateX(-100%);
    transition: transform var(--pt-move) var(--pt-ease);
  }

  html[data-pdrawer="open"] .pdrawer {
    transform: translateX(0);
  }

  /* The universal closed rule above (`html[data-pdrawer="closed"]
     .pdrawer`) carries the SAME specificity as this selector and wins the
     `visibility` value at every width - correct, the phone's closed drawer
     must leave the tab order too. But that rule's own `transition` list has
     no `transform` in it (desktop has no transform to animate), and being
     equally specific it would otherwise win the `transition` PROPERTY here
     as well, silently dropping the phone's slide-out motion. Redeclared
     here, later in the cascade, to keep both: the slide animates AND
     `visibility` still only flips once it finishes. */
  html[data-pdrawer="closed"] .pdrawer {
    transition:
      transform var(--pt-move) var(--pt-ease),
      visibility 0s linear var(--pt-move);
  }
}

@media (hover: none) {
  .pdaccount__trigger:hover,
  .pdaccount__item:hover { background: none; }

  .pdaccount__item:active { background: var(--surface-active); }

  .pdrawer-toggle:active { transform: scale(0.94); }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .pdrawer-scrim { transition: none; }
}

/* ─── Reduced motion: shared chrome lands instantly ─── */
@media (prefers-reduced-motion: reduce) {
  .pdaccount__menu {
    animation: none;
    transition: none;
  }

  .pdrawer,
  .pdaccount__trigger,
  /* The closed-state rules re-declare `transition` at higher specificity than
     the plain `.pdrawer` above (so the collapse's delayed `visibility` - and
     the phone's slide - survive the cascade); that same higher specificity
     would otherwise survive reduced-motion too and keep animating past the
     bare-class rule. Matched here so the collapse lands instantly either way.
     The external opener has the same closed-state specificity. */
  html[data-pdrawer="closed"] .pdrawer,
  .pdrawer-toggle,
  html[data-pdrawer="closed"] .pdrawer-toggle {
    transition: none;
  }

  .pdrawer-toggle:active { transform: none; }
}

/* ── The status vocabulary (BR-A3a, F1 proofs) ───
   One status vocabulary for the whole product (DESIGN.md, Controls, forms
   and state): a word plus a shape for every state, never colour alone, no
   hex, tokens only. `.status` and `.status--recorded` are defined above
   (F2d); this is the rest of the family. The Signal Rule still applies:
   `.status--recorded` is the only member that may carry the signal token
   (it marks a settled, evidenced agent act) - every other variant here is
   ink and shape, a human or neutral read, never chartreuse. */
.status--proposed::before { background: transparent; }                       /* open circle: a proposal */
.status--pending::before  { background: transparent; border-style: dashed; } /* dashed circle: a check under way */
.status--done::before     { background: currentColor; }                     /* filled neutral: a human completion */
.status--failed   { color: var(--danger-text); border-color: currentColor; }
.status--failed::before   { background: currentColor; }
.status--warning  { color: var(--warning-text); border-color: currentColor; }
.status--unknown  { color: var(--text-subtle); }
.status--unknown::before  { border-style: dotted; }
.status--needs-you { border-color: var(--line-strong); }
.status--needs-you::before { content: "!"; border: 0; width: auto; height: auto; font: 700 11px/1 var(--font-mono); }
/* AV-02 shared agent avatar presentation. The component owns the reserved
   square; surrounding controls and author labels keep their own hit areas. */
.agent-avatar {
  --agent-avatar-lit: var(--avatar-beno-lit);
  --agent-avatar-shade: var(--avatar-beno-shade);
  --agent-avatar-flat: var(--avatar-beno-flat);
  --agent-avatar-eye: var(--avatar-eye);
  --agent-avatar-highlight: var(--avatar-highlight);
  display: inline-grid;
  place-items: center;
  width: var(--agent-avatar-size, 40px);
  height: var(--agent-avatar-size, 40px);
  min-width: var(--agent-avatar-size, 40px);
  min-height: var(--agent-avatar-size, 40px);
  overflow: hidden;
  flex: none;
  vertical-align: middle;
  line-height: 0;
  color: var(--text);
}

.agent-avatar--people {
  --agent-avatar-lit: var(--avatar-people-lit);
  --agent-avatar-shade: var(--avatar-people-shade);
  --agent-avatar-flat: var(--avatar-people-flat);
}

.agent-avatar--seo {
  --agent-avatar-lit: var(--avatar-seo-lit);
  --agent-avatar-shade: var(--avatar-seo-shade);
  --agent-avatar-flat: var(--avatar-seo-flat);
}

.agent-avatar__motion,
.agent-avatar__motion > [data-avatar-svg="true"] {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
}

.agent-avatar--fallback {
  border-radius: 50%;
  background: var(--surface-elev);
}

.agent-avatar__fallback-image,
.agent-avatar__fallback-glyph {
  display: block;
  width: 100%;
  height: 100%;
}

.agent-avatar__fallback-image {
  object-fit: cover;
}

.agent-avatar__fallback-glyph {
  padding: 20%;
  color: var(--text-muted);
}

.agent-avatar__fallback-monogram {
  font: 600 0.55em/1 var(--font-sans);
  color: var(--text-muted);
}

/* ── Explicit page states ────────────────────────────────────────────────
   The caller supplies the state, copy and any action or loading geometry.
   This primitive stays quiet enough for sparse staff pages and carries no
   signal colour: a read state is not evidence that an agent acted. */
.state-notice {
  min-width: 0;
  color: var(--text-subtle);
  font: var(--t-body-sm);
}

.state-notice--inline { padding-block: var(--sp-2); }
.state-notice--section { padding-block: var(--sp-4); }

.state-notice__body > :where(:first-child, :last-child) {
  margin-block: 0;
}

.state-notice__body > :where(p + p) { margin-top: var(--sp-2); }

.state-notice__title {
  margin: 0 0 var(--sp-1);
  color: var(--text);
  font: var(--t-body-sm);
  font-weight: 600;
}

.state-notice__action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}

.state-notice__action > :is(button, a) { min-height: var(--tap-min, 44px); }

.state-notice--error { color: var(--danger-text); }

/* ── Public capability notices ──────────────────────────────────────────
   A public document's controller owns its refusal copy, status and any
   contact disclosure. This presentation adds no state or navigation. */
.capability-notice {
  width: min(100%, 460px);
  min-width: 0;
  text-align: center;
}

.capability-notice--standard {
  animation: capability-notice-rise 320ms var(--ease-out) both;
}

.capability-notice--compact { width: min(100%, 440px); }

@keyframes capability-notice-rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

.capability-notice__brand {
  color: var(--text-muted);
  margin-bottom: var(--sp-6);
}

.capability-notice__title {
  margin: 0;
  color: var(--text);
  font: var(--t-h2);
  letter-spacing: var(--tr-h2);
  text-wrap: balance;
}

.capability-notice__body {
  margin: var(--sp-3) 0 0;
  color: var(--text-muted);
  font: var(--t-body);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}

.capability-notice--compact .capability-notice__body {
  margin-top: var(--sp-4);
}

.capability-notice__aside {
  margin: var(--sp-3) 0 0;
  color: var(--text-subtle);
  font: var(--t-mono-data);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.capability-notice__contact {
  margin: var(--sp-5) 0 0;
  color: var(--text-subtle);
  font: var(--t-body-sm);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}

.capability-notice__contact a {
  position: relative;
  color: var(--text);
  overflow-wrap: anywhere;
}

.capability-notice__contact a::after {
  content: "";
  position: absolute;
  inset: -14px -6px;
}

.capability-notice__attribution {
  margin: var(--sp-10) 0 0;
  color: var(--text-faint);
  font: var(--t-mono-label);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
}

@media print {
  .capability-notice--standard { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .capability-notice--standard { animation: none; }
}

/* ── The shared conversation look: `.conv-thread` (COT-456) ──
   Practice's thread pane (`#thread-pane-body`) and Client's conversation body
   both carry `.conv-thread`: bubbles on both sides, the agent avatar in the
   gutter, the conversation type step.

   This sheet loads on every host, so every selector here needs a
   `.conv-thread` ancestor:
     - `.conv-thread X` renames a rule that was `.proom__body X` in
       room.css, at the same specificity.
     - `:where(.conv-thread) X` copies a rule that was unscoped in
       room.css or product.css, at that rule's original specificity, so
       it wins and loses the same ties it always did.

   Client loads neither product.css nor room.css, so the product.css base
   rules this look needs are copied here. Practice keeps its own unscoped
   copies of `.chat-msg--bubbled` bubbles, human/image glyphs and
   `.agent-mark--*`, because the morning bubble, the room list and the legacy
   inbox use them outside `.conv-thread`.

   The mark size reads `--conv-mark`: room.css sets it from
   `--proom-mark`, and Client sets it on its conversation body. ── */

/* product.css's `.chat-msg--agent` also lays the row out (flex, the
   mark-to-body gap, top alignment before a bubble pulls it down); Client
   needs that duplicated here since it never loads product.css. The
   max-width this shares with a bare `.chat-msg` is the separate, moved rule
   just below. */
:where(.conv-thread) .chat-msg--agent {
  display: flex;
  gap: 10px;
  align-items: flex-start;
}

/* A letter uses the WHOLE column. An <iframe>'s intrinsic width is 300px, so
   inside any box that sizes itself to its content - a flex item left at
   `flex: 0 1 auto`, an inline-block bubble - the frame's own `width: 100%`
   resolves against that 300px: the letter rendered in a ~380px sub-column
   with the rest of the 720px empty, and clipped on its right. The fix is to
   give the boxes above it a definite width to resolve against. */
.conv-thread .chat-msg,
.conv-thread .chat-msg--agent {
  max-width: 100%;
}

/* A bubble is the shape of what was said (I21). This carried `width: 100%`
   for the letter's sake and every one-line answer paid for it: eight words in
   a box 78% of the column wide, which is a panel with a sentence in it. The
   definite width moves to the bubble that actually needs one, below. */
.conv-thread .chat-msg__body {
  flex: 0 1 auto;
  width: fit-content;
  max-width: 100%;
  min-width: 0;
}

/* The agent's NAME is the agent speaking, not a label on it (I2 rule 6, I6).
   `.chat-msg__who` is the legacy console's 10px tracked mono, which in the
   room set "Anna" in the same face as a timestamp and made the answer look
   like log output. Here it is the body face; mono stays for the tape's dates
   and the fold line's length. */
.conv-thread .chat-msg__who {
  font: var(--t-body-sm);
  font-family: inherit;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-muted);
  /* One hairline of air under the name (I14): the name and the first line of
     the answer are one utterance, and five pixels of gap made two. */
  margin: 0 0 var(--sp-1);
}

/* Muted, not paper (I14). In the paper ink the name was the loudest thing in
   the bubble and read as a heading over the words rather than as the person
   saying them; the mockup sets both the name and its addressing below the
   prose it introduces. */
.conv-thread .chat-msg__who b {
  font-weight: 500;
  color: var(--text-muted);
}

/* And the same at full height (I14): `.chat-msg--agent` is head-aligned in
   the console's thread, where a mark labels a log line. Here it is a person
   under their own words. */
.conv-thread .chat-msg--bubbled {
  align-items: flex-end;
}

/* Unfolding is not an arrival: the body appears where the scroll is taking
   the reader, and a second motion under a moving page is the jump (I5 item
   8). Nothing in here animates. */
.conv-thread .chat-msg--agent {
  animation: none;
}

/* Two sides, ONE bubble (I10b 9.1). The operator is on the right in the
   tinted step the console already draws; every other participant - an agent,
   a colleague - is on the left in the same flat shape, under their own mark
   and name. The name and the objects live INSIDE the bubble, which is what
   makes an answer with a table under it read as one thing somebody said
   rather than as three stacked rows.

   The agent's side is the OBJECT step, --surface-card (2026-09-13; COT-234b,
   BR-A2b and I14 before it). It was `--surface` - the page's own colour -
   so on paper an agent's bubble was a hairline around nothing; then
   --surface-elev, which on paper is a tier BELOW the page, so the bubble
   read as a recessed panel. On --surface-card it is a warm near-white on
   paper and the top step in the dark: the agent hands you a card, it does
   not write on the wall. The operator's side keeps the page's tint
   (--you-tint), which is what now tells the two sides apart. DESIGN.md's Elevation rules out a lift or a tail corner for
   that card now - flat, ruled surfaces, one radius - so the shape that used
   to say "bubble" is the raised step and the hairline alone.

   room.css keeps the original, unscoped copy of this rule (it also
   dresses the review card's own bubble, outside `.conv-thread`); this is
   the `:where(.conv-thread)`-scoped copy Client reads instead. It must come
   after the `.conv-thread .chat-msg__body` rule above: both set max-width
   at the same real specificity, and this one has to win the tie for Client
   the way it always won it for Practice by sitting later in one sheet. */
:where(.conv-thread) .chat-msg--bubbled .chat-msg__body {
  background: var(--surface-card);
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-3);
  /* The padding a spoken paragraph takes. --sp-2/--sp-3 was a chip's air
     around a sentence, and at the bubble's new type it read tight. */
  padding: var(--sp-3) var(--sp-4);
  /* And speech reads a step up from the register (I16): --t-body is 15px,
     the size a table of rows is set in. */
  font: var(--t-body-lg);
  /* A definite width to resolve against (I5 item 7): `width: 100%` under a
     `max-width` is what a letter's iframe measures itself from, and the cap
     is the bubble's own measure - 78% of the column, or 68ch of reading
     measure (COT-234b, BR-A2b), whichever is narrower, so a wide monitor
     does not hand back a paragraph nobody can read end to end. */
  max-width: min(78%, 68ch);
}

:where(.conv-thread) .chat-msg--bubbled.chat-msg--owner {
  max-width: 78%;
}

/* product.css's `.chat-bubble` is the shared primitive the operator's own
   side still renders through: its box model (inline sizing, left-set
   reading inside a right-anchored shape, the border's own width and style,
   the speech-preserving whitespace) is what the override just below only
   tints and rounds. Client needs it duplicated for the same reason as the
   rules above it. */
:where(.conv-thread) .chat-bubble {
  display: inline-block;
  text-align: left;
  color: var(--text);
  border-style: solid;
  border-width: 1px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* And the operator's own side is TINTED (I16), on the SAME shape as the
   agent's now (COT-234b, BR-A2b): `--you-tint`/`--you-rule` are the ladder's
   own elevated step and hairline (COT-245 retired the olive hue axis they
   used to carry), so the only thing that still tells the two sides apart is
   which edge of the room they stand on. `.chat-bubble` is a shared console
   primitive whose own base rule (product.css, duplicated above for a host
   that does not load it) still draws the old tail corner; this override,
   scoped to the shared conversation layer, is what flattens it here without
   touching the primitive itself. */
.conv-thread .chat-msg--owner .chat-bubble {
  background: var(--you-tint);
  border-color: var(--you-rule);
  border-radius: var(--r-3);
  padding: var(--sp-3) var(--sp-4);
  font: var(--t-body-lg);
  max-width: 68ch;
}

/* product.css's `.chat-msg--owner` sets the row's own alignment in the
   column - right-anchored, its own reading direction - before this look
   turns it into the flex row its mark needs, just below. Client needs the
   base duplicated since it never loads product.css. */
:where(.conv-thread) .chat-msg--owner {
  align-self: flex-end;
  max-width: 80%;
  margin-left: auto;
  text-align: right;
}

/* The operator's own mark (A6): their round face or initials on their own
   side - after the bubble, at its tail corner - so the person never vanishes
   from a message they sent. The row is a block in the console's thread, where
   the bubble is the only thing on it; here it becomes the flex row the mark
   needs, right-aligned and foot-aligned like the agent's own side. It wraps so
   a message's file chips keep their line above the bubble rather than being
   pulled up beside it. */
.conv-thread .chat-msg--owner {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.conv-thread .chat-msg--owner .chat-msg__files {
  flex: 0 0 100%;
}

/* And the bubble leaves the mark its width. A flex row breaks its lines on
   each item's UNSHRUNK size, so a long message - which is every message wide
   enough to matter - claimed the whole line on its own and dropped the mark
   underneath it, which is the vanishing act this commit is about, one shape
   along. Clamped, the two share the line and the bubble shrinks into what is
   left. */
.conv-thread .chat-msg--owner:has(.chat-msg__glyph--you) .chat-bubble {
  max-width: calc(100% - var(--conv-mark) - var(--sp-2));
}

.conv-thread .chat-msg--owner .chat-msg__glyph--you {
  flex: none;
  margin-left: var(--sp-2);
  align-self: flex-end;
}

/* product.css's `.chat-msg__glyph` sets the tile's whole shape (box, border,
   background, centering, the mono initial's face); Client needs it
   duplicated. It has to come BEFORE the human/image/agent-hue overrides
   just below: all four sit at the same real (`:where`-stripped) one-class
   specificity, and each override relies on outranking this shape by
   landing later, the way it always outranked product.css's own rule by
   sheet order. */
:where(.conv-thread) .chat-msg__glyph {
  flex: none;
  border-radius: var(--r-2);
  border: 1px solid var(--surface-rule);
  background: var(--surface-elev);
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  color: var(--signal-text);
  overflow: hidden;
}

/* A person's mark is ROUND with their initials; an agent's is the square the
   console already draws. The shape is the whole difference, so it carries no
   colour of its own: chartreuse means an agent acted, and a colleague reading
   a client is not an agent (DESIGN.md, The Signal Rule).

   room.css keeps the original, unscoped copy of this rule (it also
   marks a colleague's face in the legacy thread inbox, outside `.conv-
   thread`); this is the `:where(.conv-thread)`-scoped copy Client reads
   instead. */
:where(.conv-thread) .chat-msg__glyph--human {
  border-radius: 999px;
  color: var(--text);
  font-size: 11px;
}

/* Their face in the same place, filling the mark's box. The width and height
   stay the mark's own, so a bubble beside a photo sits exactly where a bubble
   beside a pair of initials does. */
:where(.conv-thread) .chat-msg__glyph--img {
  display: block;
  object-fit: cover;
  padding: 0;
}

/* Each agent in its own hue (I16). The mark was the same square with a
   different letter in it whichever agent was speaking, so a conversation with
   two colleagues in it read as one voice. Beno keeps the signal ink his marks
   have always worn - a mark IS an agent, which is what the Signal Rule
   reserves the colour for - and Anna takes her own token. The letter and a
   1px ring, nothing else: a filled mark would be a badge, and the shape
   already says agent (round is a person). `ChatComponents.agent_class/1` is
   the one place the mapping lives.

   room.css keeps the original, unscoped copy of this rule (it also
   colours the room-list card's own `.proom__x-mark` and the legacy thread
   inbox's marks, neither inside `.conv-thread`); this is the `:where(.conv-
   thread)`-scoped copy Client reads instead - `.proom__x-mark` never
   appears there, so this copy only needs the `.chat-msg__glyph` pairing. */
:where(.conv-thread) .agent-mark--beno {
  color: var(--signal-text);
  border-color: color-mix(in oklab, var(--signal-text) 30%, transparent);
}

:where(.conv-thread) .agent-mark--people {
  color: var(--agent-people);
  border-color: color-mix(in oklab, var(--agent-people) 30%, transparent);
}

:where(.conv-thread) .agent-mark--seo {
  color: var(--agent-seo);
  border-color: color-mix(in oklab, var(--agent-seo) 30%, transparent);
}

/* And the agent's drawn face fills whichever tile it was asked for
   (`ChatComponents.agent_portrait/1`'s own doc): a share of the box rather
   than a pixel size, from product.css's `.agent-portrait`, duplicated here
   for the same reason as the rules above it. */
:where(.conv-thread) .agent-portrait {
  width: 64%;
  height: 64%;
  display: block;
}

/* A photographed face fills the tile instead: same square, same hue ring,
   the picture is the content. Duplicates product.css's `.agent-portrait--photo`
   for the same reason as the plain glyph above it, and after it in this
   sheet too, so a node wearing both classes (every photographed agent glyph)
   resolves the same way regardless of which sheet a reader checks. */
:where(.conv-thread) .agent-portrait--photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The marks are the column's left edge, so they are sized here and nowhere
   else (I16): the console's own 24px glyph is a label beside a log line, and
   - in Practice - two rails outside this shared layer, the handoff thread's
   indent and the Worked line's start, are measured in the room's own
   `--proom-mark` and move with it. A host with no `--proom-mark` sets
   `--conv-mark` on its own root; 48px is the fallback while none does. */
.conv-thread .chat-msg__glyph {
  width: var(--conv-mark, 48px);
  height: var(--conv-mark, 48px);
}

/* Every agent reply carries its author in the message gutter. Single-agent
   conversations can omit repeated names, but never the avatar itself. */
.conv-thread[data-avatar-authorship="single"] .chat-msg--agent > .chat-msg__glyph,
.conv-thread[data-avatar-authorship="multi"] .chat-msg--agent > .chat-msg__glyph {
  visibility: visible;
  align-self: flex-start;
  margin-top: var(--sp-1);
}

.conv-thread[data-avatar-authorship="single"] .chat-msg--agent .chat-msg__who {
  display: none;
}

.conv-thread[data-avatar-authorship="multi"] .chat-msg__speaker-name {
  position: static;
  display: block;
  width: auto;
  height: auto;
  margin: 0 0 var(--sp-1);
  padding: 0;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
  font: var(--t-body-sm);
  color: var(--text-muted);
}

/* And the words in a bubble read at the conversation's own size (I16). The
   markdown renderer sets its paragraphs at a literal 14px, which is the
   console panel's density, so the bubble's own `font` never reached them.
   The revealed WORK keeps its smaller step in whichever sheet still draws
   it - its own rule is more specific - because unfolding a turn's working
   is not more conversation. */
.conv-thread .chat-md__p {
  font: var(--t-body-lg);
}

/* A Moodstone avatar is its own shape, so the glyph that holds it drops the
   mark's box. Copied from product.css at the same specificity for Client. */
:where(.conv-thread) .chat-msg__glyph:has(> .agent-avatar--moodstone) {
  min-width: max-content;
  min-height: max-content;
  background: none;
  border: 0;
  overflow: visible;
}

/* Phone widths. Practice gets these from its own room.css (559.98px) and
   product.css (900px) rules, which load after this sheet; Client loads
   neither, so the same widths are copied here at the originals'
   specificity. */
@media (max-width: 900px) {
  :where(.conv-thread) .chat-msg--owner {
    max-width: 88%;
  }
}

@media (max-width: 559.98px) {
  :where(.conv-thread) .chat-msg--bubbled .chat-msg__body,
  :where(.conv-thread) .chat-msg--bubbled.chat-msg--owner {
    max-width: 88%;
  }
}

/* ── Onboarding status (COT-514) ─────────────────────────────────────────
   One quiet control in the head of a client's conversation (the Practice
   room, the console chat panel) while that client is onboarding, and a
   disclosure listing only what is still open. Human control, so neutral:
   no Action colour. Progress is a word and a count, the meter only repeats
   it for the eye. */
.onbstatus {
  position: relative;
  flex: none;
}

.onbstatus__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 36px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-3);
  background: var(--surface-card);
  color: var(--text);
  font: var(--t-body-sm);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--pt-fast) var(--pt-ease), border-color var(--pt-fast) var(--pt-ease);
}

.onbstatus__toggle:hover {
  background: var(--surface-hover);
}

.onbstatus__toggle:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.onbstatus__toggle[aria-expanded="true"] {
  background: var(--surface-active);
  border-color: var(--text-subtle);
}

.onbstatus__meter {
  display: inline-flex;
  gap: 3px;
}

.onbstatus__step {
  width: 12px;
  height: 4px;
  border-radius: 2px;
  background: var(--line-strong, var(--surface-rule));
}

.onbstatus__step--done {
  background: var(--text);
}

.onbstatus__count {
  font: var(--t-mono-data);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.onbstatus__caret {
  color: var(--text-muted);
  transition: transform var(--pt-fast) var(--pt-ease);
}

.onbstatus__toggle[aria-expanded="true"] .onbstatus__caret {
  transform: rotate(180deg);
}

.onbstatus__panel {
  position: absolute;
  top: calc(100% + var(--sp-2));
  right: 0;
  z-index: var(--z-menu);
  width: min(340px, calc(100vw - 2 * var(--sp-4)));
  padding: var(--sp-1) 0;
  background: var(--surface-overlay, var(--surface-card));
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-4);
  box-shadow: var(--shadow-overlay);
  animation: pmenu-in var(--pt-fast) var(--pt-ease);
}

.onbstatus__panel[hidden] {
  display: none;
}

/* Beside a composer: the disclosure opens upward, and the control shrinks to
   the chip row it stands in. */
.onbstatus--up {
  position: static;
}

/* Anchored to the positioned composer block it stands in, so it spans the
   composer's own width and never runs off a narrow panel. */
.onbstatus--up .onbstatus__panel {
  top: auto;
  bottom: calc(100% + var(--sp-2));
  left: var(--sp-3);
  right: var(--sp-3);
  width: auto;
}

.onbstatus--compact .onbstatus__toggle {
  min-height: 28px;
  padding: 0 var(--sp-2);
}

.onbstatus__items {
  list-style: none;
  margin: 0;
  padding: 0;
}

.onbstatus__item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-3) var(--sp-4);
}

.onbstatus__item + .onbstatus__item {
  border-top: 1px solid var(--surface-rule);
}

.onbstatus__item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 28px;
}

/* A step's own verb: quiet, on the step it acts on, never Action colour. */
.onbstatus__act {
  flex: none;
  padding: 3px var(--sp-2);
  font: var(--t-body-sm);
}

.onbstatus__seat {
  border-top: 1px solid var(--surface-rule);
}

/* One inline flow at a time, under the step it belongs to. */
.onbstatus__flow {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  padding: var(--sp-3);
  border-radius: var(--r-3);
  background: var(--surface-elev);
}

.onbstatus__flow-text,
.onbstatus__flow-quote,
.onbstatus__flow-error {
  margin: 0;
  font: var(--t-body-sm);
  color: var(--text);
}

.onbstatus__flow-quote {
  color: var(--text-muted);
}

.onbstatus__flow-error {
  color: var(--danger-text, var(--text));
}

.onbstatus__mono {
  font: var(--t-mono-data);
  overflow-wrap: anywhere;
}

.onbstatus__form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.onbstatus__input {
  width: 100%;
  box-sizing: border-box;
  min-height: 36px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-strong, var(--surface-rule));
  border-radius: var(--r-2);
  background: var(--surface-card);
  color: var(--text);
  font: var(--t-body-sm);
}

.onbstatus__input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

.onbstatus__flow-verbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}

.onbstatus__flow-verbs .btn[disabled] {
  opacity: 0.6;
  cursor: default;
}

.onbstatus__item-label {
  font: var(--t-body-sm);
  color: var(--text);
}

.onbstatus__item-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sp-2);
  font: var(--t-body-sm);
  color: var(--text-muted);
}

.onbstatus__since::before {
  content: "\00b7";
  margin-right: var(--sp-2);
}

.onbstatus__note,
.onbstatus__checks {
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  font: var(--t-body-sm);
  color: var(--text-muted);
}

.onbstatus__items + .onbstatus__checks,
.onbstatus__seat + .onbstatus__checks {
  border-top: 1px solid var(--surface-rule);
}

.onbstatus__checks-label {
  display: block;
  color: var(--text);
}

/* COT-515: the home page's list of onboarding clients. Flat ruled rows,
   quiet beside the page's primary ask; the progress repeats the room's own
   control vocabulary. */
.onblist {
  width: 100%;
  margin-top: var(--sp-6);
  /* Hosts centre their welcome block; a list reads from the left. */
  text-align: start;
}

.onblist__title {
  margin: 0 0 var(--sp-2);
  font: var(--t-body-sm);
  font-weight: 500;
  color: var(--text-muted);
}

.onblist__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--surface-rule);
}

.onblist__rows > li {
  border-bottom: 1px solid var(--surface-rule);
}

.onblist__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--sp-4);
  align-items: baseline;
  min-height: var(--tap-min);
  padding: var(--sp-3) var(--sp-2);
  color: var(--text);
  text-decoration: none;
  border-radius: var(--r-2);
  transition: background var(--pt-fast) var(--pt-ease);
}

.onblist__row:hover {
  background: var(--surface-hover);
}

.onblist__row:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

.onblist__name {
  font: var(--t-body-sm);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.onblist__progress {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.onblist__next {
  grid-column: 1 / -1;
  font: var(--t-body-sm);
  color: var(--text-muted);
}

.onblist__owner::before {
  content: "\00b7";
  margin: 0 var(--sp-2);
}

@media (pointer: coarse) {
  .onbstatus__toggle,
  .onbstatus__act,
  .onbstatus__flow-verbs .btn,
  .onbstatus__input {
    min-height: var(--tap-min);
  }
}

@media (max-width: 559.98px) {
  .onbstatus__meter {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .onbstatus__panel {
    animation: none;
  }

  .onbstatus__caret,
  .onbstatus__toggle {
    transition: none;
  }
}

/* ── The documents card and list row (DH-09a, COT-608) ──
   Shared since COT-611 (DH-11): the staff canvas and card (product.css
   hosts) and the client host's Documents screen (client.css) draw the same
   row, so it lives here with the preview and mail frames.
   `CotrugliWeb.DocumentComponents`, built to DH-07's approved proofs
   (workspace/design/surface-proofs/documents-*.html). One row in the card and
   in the canvas list. A list, not a table: the columns are a reading order,
   not a comparison grid, and a phone reflows it to two lines without a
   stacked-table fallback. */
.doc-list { margin: 0; padding: 0; list-style: none; }
.doc-row {
  position: relative;
  display: grid;
  grid-template-columns: 4.75em 16px minmax(0, 1fr) auto;
  grid-template-areas:
    "date mark main side"
    "open open open open";
  column-gap: var(--sp-3);
  align-items: baseline;
  padding: var(--sp-2) var(--pad-panel);
  border-top: 1px solid var(--surface-rule);
}
.doc-list > .doc-row:first-child { border-top: 0; }
.doc-row:hover { background: var(--surface-hover); }
.doc-row[aria-current="true"],
.doc-row--open { background: var(--surface-elev); }
.doc-row[aria-current="true"]::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 2px; background: var(--text); }
.doc-row__date { grid-area: date; font: var(--t-mono-data); font-variant-numeric: tabular-nums; color: var(--text-subtle); white-space: nowrap; }
.doc-row__mark { grid-area: mark; align-self: start; margin-top: 2px; width: 16px; height: 16px; color: var(--text-muted); }
.doc-row__main { grid-area: main; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* The title IS the open control. Its ::after covers the row, so the whole
   row is the target (44px floor on a coarse pointer, below) while the
   accessible name stays the title plus its kind. */
.doc-row__open {
  all: unset;
  font: var(--t-body-sm);
  font-weight: 550;
  color: var(--text);
  overflow-wrap: anywhere;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 2px;
}
.doc-row__open::after { content: ""; position: absolute; inset: 0; }
.doc-row__open:hover { text-decoration-color: currentColor; }
.doc-row__open:focus-visible { outline: none; }
.doc-row:has(.doc-row__open:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.doc-row__title { font: var(--t-body-sm); font-weight: 550; color: var(--text); overflow-wrap: anywhere; }
.doc-row__meta { font: var(--t-body-sm); color: var(--text-muted); overflow-wrap: anywhere; }
.doc-row__meta .doc-row__kind { color: var(--text-subtle); }
.doc-row__side { grid-area: side; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; text-align: right; }
.doc-row__amount { font: var(--t-mono-data); font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }
.doc-row__amount .money__ccy { font-size: 0.85em; }
/* What opening gives. A word, always; the unavailable ones also drop the
   open control, which is the shape that says "cannot open". */
.doc-gives { font: var(--t-micro); color: var(--text-subtle); white-space: nowrap; }
.doc-gives[data-availability="unavailable"] { color: var(--text-muted); }
.doc-gives[data-reason="quarantined"],
.doc-gives[data-reason="missing"] { color: var(--warning-text); }
.doc-gives__glyph { width: 12px; height: 12px; vertical-align: -2px; margin-right: 3px; }
/* A row's extras line: the reason it cannot open, redactions, generations. */
.doc-row__extra { grid-area: open; min-width: 0; padding-left: calc(4.75em + 16px + 2 * var(--sp-3)); }
.doc-row__note { margin: var(--sp-1) 0 0; font: var(--t-micro); color: var(--text-muted); }
.doc-redaction { font: var(--t-mono-data); color: var(--text-muted); border: 1px dashed var(--line-strong); border-radius: var(--r-1); padding: 0 4px; }
/* An export run more than once: its generations, newest first, each dated. */
.doc-gens { margin: var(--sp-1) 0 0; padding: 0; list-style: none; position: relative; z-index: var(--z-raised); }
.doc-gen { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-3); padding: 6px 0; border-top: 1px solid var(--surface-rule); font: var(--t-body-sm); color: var(--text-muted); }
.doc-gen:first-child { border-top: 0; }
.doc-gen__when { font: var(--t-mono-data); font-variant-numeric: tabular-nums; color: var(--text); }
.doc-gen__label { color: var(--text-muted); }
.doc-gen__files { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
/* The notices (empty, read failure) sit on the card's own gutter. */
.doc-notice { padding-inline: var(--pad-panel); }
.doc-notice__action { margin-top: var(--sp-2); }
@media (max-width: 559.98px) {
  /* The date leaves the lead column on a phone: it joins the amount and
     what opening gives on the row's second line, so the title keeps the
     width. */
  .doc-row {
    grid-template-columns: 16px auto minmax(0, 1fr);
    grid-template-areas:
      "mark main main"
      ".    date side"
      "open open open";
    column-gap: var(--sp-2);
  }
  .doc-row__date { margin-top: 2px; }
  .doc-row__side { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 2px var(--sp-3); text-align: left; margin-top: 2px; }
  .doc-row__extra { padding-left: calc(16px + var(--sp-2)); }
}
@media (pointer: coarse) {
  .doc-row { min-height: var(--tap-min); }
}

/* ── A held document, opened (DH-08b, COT-607) ──
   `CotrugliWeb.DocumentPreviewComponents`: the stored email's envelope and
   cut notice, the sandboxed preview frame, and the file tile. Shared by the
   client page's Documents tab, the canvas and the room's card, so the rules
   live here rather than in one surface's sheet. Class names are the DH-07
   proof's (`workspace/design/surface-proofs/documents-proof.css`). */
.doc-open__meta {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 2px var(--sp-3);
  margin: 0 0 var(--sp-3);
  font: var(--t-body-sm);
}
.doc-open__meta dt { color: var(--text-subtle); }
.doc-open__meta dd { margin: 0; color: var(--text); overflow-wrap: anywhere; }
.doc-open__meta .mono { font: var(--t-mono-data); font-variant-numeric: tabular-nums; }
.doc-mail .pmail__text { max-height: none; overflow: visible; max-width: none; font: var(--t-body-sm); }
.doc-open__cut {
  margin: var(--sp-2) 0 0;
  padding: var(--sp-1) var(--sp-2);
  border-left: 2px solid var(--line-strong);
  font: var(--t-body-sm);
  color: var(--text-muted);
}

.doc-preview { margin: 0; min-width: 0; }
.doc-preview__frame {
  max-width: 420px;
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-2);
  background: var(--surface-elev);
  padding: var(--sp-3);
}
.doc-preview__iframe {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 0;
  background: var(--surface);
}
.doc-preview__caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-3);
  margin-top: var(--sp-2);
  font: var(--t-body-sm);
  color: var(--text-muted);
}
.doc-preview__caption .mono { font: var(--t-mono-data); color: var(--text-subtle); overflow-wrap: anywhere; }

.doc-tile-wrap { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); min-width: 0; }
.doc-tile {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-3);
  align-items: center;
  width: 100%;
  max-width: 420px;
  padding: var(--sp-3);
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-2);
  background: var(--surface);
}
.doc-tile__glyph {
  grid-row: span 2;
  width: 40px;
  height: 48px;
  display: grid;
  place-items: center;
  border: 1px solid var(--surface-rule);
  border-radius: var(--r-1);
  color: var(--text-muted);
}
.doc-tile__name { font: var(--t-mono-data); color: var(--text); overflow-wrap: anywhere; }
.doc-tile__meta { font: var(--t-body-sm); color: var(--text-muted); }
