/* Integra Workbench — component styles.
 * Consumes semantic tokens from tokens.css only. No raw hex here
 * (enforced by tools/css_gate.sh). */

/* ── Vendored type: IBM Plex Sans (UI/prose) + IBM Plex Mono (technical data).
 * Self-hosted under /static/vendor/fonts so the workbench renders offline;
 * font-display:swap falls back to the system stack until the file loads. ── */
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 400; font-display: swap; src: url("/static/vendor/fonts/ibm-plex-sans-400.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 500; font-display: swap; src: url("/static/vendor/fonts/ibm-plex-sans-500.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 600; font-display: swap; src: url("/static/vendor/fonts/ibm-plex-sans-600.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/static/vendor/fonts/ibm-plex-mono-400.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap; src: url("/static/vendor/fonts/ibm-plex-mono-500.woff2") format("woff2"); }

*, *::before, *::after { box-sizing: border-box; }

/* ── Base layer ── */
body {
  font-family: var(--font-sans);
  font-size: var(--font-size-md);
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-primary);
  background: var(--color-bg-page);
  margin: 0;
  overflow-wrap: break-word;
}

h1, h2, h3, h4 { line-height: 1.3; }
h1 { font-size: 18px; font-weight: 650; }
h2 { font-size: 16px; font-weight: 650; }
h3 { font-size: 15px; font-weight: 600; }
h4 { font-size: var(--font-size-sm); font-weight: 600; }

.code-token {
  font-family: var(--font-mono);
  font-size: 0.92em;
  background: var(--color-bg-code);
  padding: 1px var(--space-xs);
  border-radius: var(--radius-sm);
}

pre, .code-block {
  font-family: var(--font-mono);
  font-size: 0.92em;
  background: var(--color-bg-code);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  max-width: 100%;
}

.overflow-anywhere { overflow-wrap: anywhere; }

/* Tabular numerals (narrative brief U6): codes, counts, ids, and ages align
 * digit-for-digit. body sets the default; these surfaces restate it so the
 * alignment survives any future font shorthand reset on an ancestor. */
.fault-entry, .chip-gist, .dive-cite, .dive-ticket-meta,
.ticket-id, .conv-meta {
  font-variant-numeric: tabular-nums;
}

/* ════════════════════════════════════════════════════════════════════
 * Component layer (redesign U4)
 * Cards, headers, badges, chips. Every session surface composes these;
 * legacy class names (.panel, .delta-badge, .conv-flag) keep
 * their JS contracts and pick up the shared shape via grouped selectors.
 * This layer sits before the section styles so per-surface rules can
 * override the shared shape where they need to.
 * ════════════════════════════════════════════════════════════════════ */

/* Card: the one surface shape. Neutral at rest — no accent edges. */
.card {
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-lg);
}
.card h4 { margin: var(--space-md) 0 var(--space-sm); font-size: var(--font-size-sm); }

/* Card header: section-title register — small, heavy, tracked-out. */
.card-header {
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-secondary);
}

/* Badge / chip: one pill shape; color comes only from a status modifier
 * (tint background + matching text), never structure. Grayscale at rest. */
.badge, .chip, .delta-badge, .conv-flag {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
  font-size: 11px;
  font-weight: 600;
  padding: 1px var(--space-sm);
  border-radius: 999px;
  white-space: nowrap;
  background: var(--color-bg-code);
  color: var(--color-text-secondary);
}
.badge-fault    { background: var(--color-status-fault-bg);    color: var(--color-status-fault); }
.badge-warning  { background: var(--color-status-warning-bg);  color: var(--color-status-warning); }
.badge-waiting  { background: var(--color-status-waiting-bg);  color: var(--color-status-waiting); }
.badge-resolved { background: var(--color-status-resolved-bg); color: var(--color-status-resolved); }

/* Diagnostic / preformatted content scrolls sideways, never wraps the layout.
 * Shared shape comes from the grouped `pre, .code-block` rule above. */
.code-block {
  white-space: pre;
  margin-top: var(--space-xs);
}

/* Shared focus treatment for interactive elements. Summary elements keep the
   loud ring for keyboard focus only: some browsers leave a details summary
   focused (and :focus-visible) after a mouse click, which parked a persistent
   accent box around the case-memory bar (IW-53); chat.js blurs mouse clicks. */
button:focus-visible, summary:focus-visible, a:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}
summary:focus:not(:focus-visible) { outline: none; }

/* ════════════════════════════════════════════════════════════════════
 * App shell (redesign U3) — grid: 48px header row over a 280px queue
 * rail plus the case pane. Rail collapses to 56px icon mode by swapping
 * the column width var (wired in rail.js, IW-48).
 * ════════════════════════════════════════════════════════════════════ */
.shell {
  display: grid;
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  grid-template-rows: var(--header-height) minmax(0, 1fr);
  height: 100dvh;
  position: relative;
}
.shell.rail-collapsed { --rail-width: var(--rail-width-collapsed); }
.shell.shell-no-rail { grid-template-columns: minmax(0, 1fr); }
/* Width eases; content does not. .rail-list/.rail-header/.rail-footer hide
   outright below (IW-259) so nothing partial is visible while the width is
   still moving between the two states. */
@media (prefers-reduced-motion: no-preference) {
  .shell { transition: --rail-width var(--motion-enter) var(--ease-out); }
}
/* While the operator drags the edge (IW-390) the width has to follow the
   pointer, not ease after it, and a drag across the case pane must not start
   selecting its text. rail.js sets and clears the class around the drag. */
.shell.rail-resizing { transition: none; user-select: none; cursor: col-resize; }
/* Over the pane (IW-467). On a window where the pane beside the open rail would
   be narrower than --pane-width-min, the floor the screens inside it are built
   to, the rail stops pushing the pane. The grid keeps only the collapsed strip's
   column, so the pane holds the rest of the window whatever the rail does, and
   an open rail lies over the pane's left edge at its chosen width, raised on the
   theme's shadow so its edge reads as an edge and not a column rule. Placed
   from the shell's own box like the resize handle below (auto placement, so the
   grid area is not its containing block), starting under the header. It sits
   under the handle and the chevron in stacking order, which keep following
   --rail-width and so stay on the rail's edge in both postures; a popover the
   pane raises on its own interaction still paints over it, since that is what
   the operator asked for. rail.js decides the posture and sets the class; over
   the pane the rail starts collapsed, a choice made there is remembered apart
   from the desktop one, and picking a ticket, a press outside the rail or
   Escape folds an open overlay away. */
.shell.rail-overlay { grid-template-columns: var(--rail-width-collapsed) minmax(0, 1fr); }
.shell.rail-overlay:not(.rail-collapsed) .queue-rail {
  grid-area: auto;
  position: absolute;
  top: var(--header-height);
  bottom: var(--spine-row, 0px);
  left: var(--spine-column, 0px);
  width: var(--rail-width);
  z-index: 3;
  box-shadow: var(--shadow-raised);
}

/* ── Header ── */
/* The 2px keyline is the shell's one deliberate brand moment (IW-50). */
.site-header {
  grid-column: 1 / -1;
  background: var(--color-bg-inverse);
  color: var(--color-text-inverse);
  border-bottom: 2px solid var(--color-accent);
  padding: 0 20px;
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  min-height: 0;
  /* The header is its own width container (IW-467), the shell's second after
     the case pane below. The pane's steps cannot serve it: the header spans the
     window whatever the rail does, so it is measured by its own width. The one
     rule keyed to it drops the second word of the product name where the row
     is too tight to hold the name, the page links and the user menu on one
     line; a window media query would say the same thing and is kept out of
     this sheet on principle. */
  container: header / inline-size;
}
.site-logo {
  font-weight: 600;
  font-size: 15px;
  flex: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  letter-spacing: -0.01em;
  color: inherit;
  text-decoration: none;
}
.site-logo-mark { width: 22px; height: 22px; display: block; flex: none; color: var(--color-accent); }
.site-logo-mark svg { width: 100%; height: 100%; display: block; }
/* The product name never wraps (IW-467): it used to break as "Integra" over
   "Workbench" once the links beside it took the width. It is one line at every
   width, and below the header's tight step it shows the first word alone. The
   second word is taken out of the line the way .visually-hidden takes text
   out, not with display: none, so the link still reads as the whole name to a
   screen reader at every width. The declarations are .visually-hidden's, one
   for one: a class cannot be added by a container query, so the recipe is
   restated here and a test holds the two copies equal. */
.site-logo-name { white-space: nowrap; }
@container header (max-width: 720px) {
  .site-logo-word {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}
/* At phone width (IW-537) the row ran past a 390px window, pushing the theme
   toggle and the user menu off the dark bar and the page sideways. Below the
   header's tight step, the pane's own 560px, the bar packs closer: the logo
   keeps its mark alone. The name leaves the line by .visually-hidden's recipe,
   like the logo's second word above, so the link keeps its whole name for a
   screen reader; the test holds this copy equal to it too. A container query
   cannot restyle the container, so the header keeps its own padding and gap,
   and the items give them back instead: the end items take the header's 20px
   side padding down to --space-md, and each item after the logo returns half
   the gap. Both literals are the rules' own; the test holds them equal, since a
   custom property here would need a token. The rules are scoped to the header
   because the rules they adjust come later in the sheet. The user name
   ellipsizes, so the page itself never grows wider than the window. The page
   links that once shared this row moved to the spine (IW-624). */
@container header (max-width: 560px) {
  .site-header > .site-logo { margin-left: calc(var(--space-md) - 20px); }
  .site-header > .user-menu { margin-right: calc(var(--space-md) - 20px); }
  .site-header > .theme-toggle-slot,
  .site-header > .user-menu { margin-left: calc(var(--space-sm) - var(--space-lg)); }
  .site-logo-name {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .site-header .theme-toggle { padding: 4px; min-width: 0; }
  .site-header .user-menu-button { gap: var(--space-xs); padding: 4px var(--space-xs); }
}
.logout-form { display: inline; }

/* User menu (IW-322): the signed-in name is a button, and its menu holds
   My settings, Admin settings (operators only) and Sign out. Behaviour is in
   user_menu.js; the list is shown and hidden through its hidden attribute.
   The list is right-aligned under the button and capped to the viewport width
   less the header padding, so at phone width it can neither run off the left
   edge nor make the page scroll sideways.
   The list hangs from the header, not from the button (IW-354). The button is
   shorter than the header, so a list measured from the button started above
   the keyline and cut it. .user-menu therefore stretches to the header's full
   height and centres the button inside itself, and the list drops from there.
   top: 100% of a child ends at the header's content box, which stops above
   the 2px keyline, so the gap the other popovers use is added to the
   keyline's own 2px and the list clears the line by that gap.
   The stretched box takes no clicks itself, only the button and the list do.
   A press outside #user-menu closes the menu (dismiss.js, IW-415), and the
   header strips above and below the button have to stay outside for that.
   The menu may shrink (IW-537): on a bar too narrow for everything, a long name
   ends in an ellipsis rather than pushing the button past the window, and it
   gives way well before a page link scrolls out of sight in the nav, since the
   operator knows their own name and a link cut off is a page hidden. The
   floor keeps the caret and a stub of the name inside the button; without it
   a squeezed button drew its caret past the window. */
.user-menu {
  position: relative;
  flex: 0 8 auto;
  min-width: 4em;
  align-self: stretch;
  display: flex;
  align-items: center;
  pointer-events: none;
}
.user-menu-button, .user-menu-list { pointer-events: auto; }
/* The button is a header control like the page links (IW-530): their height,
   their resting tone and their hover fill. */
.user-menu-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  max-width: 50vw;
  box-sizing: border-box;
  height: 28px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-header-text-rest);
  cursor: pointer;
  font-size: var(--font-size-sm);
  padding: 4px var(--space-sm);
}
.user-menu-button:hover,
.user-menu-button[aria-expanded="true"] {
  color: var(--color-text-inverse);
  background: var(--color-header-control-hover);
}
.user-menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-menu-caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
}
.user-menu-button[aria-expanded="true"] .user-menu-caret { transform: rotate(180deg); }
.user-menu-list {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 40;
  min-width: 180px;
  max-width: calc(100vw - 2 * var(--space-lg));
  margin: calc(2px + var(--space-xs)) 0 0;
  padding: var(--space-xs) 0;
  list-style: none;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
}
.user-menu-list[hidden] { display: none; }
.user-menu-list a,
.user-menu-list button {
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--color-text-primary);
  cursor: pointer;
  font: inherit;
  font-size: var(--font-size-sm);
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
}
.user-menu-list a:hover, .user-menu-list a:focus,
.user-menu-list button:hover, .user-menu-list button:focus { background: var(--color-bg-hover); }
.user-menu-list a:focus-visible,
.user-menu-list button:focus-visible { outline-offset: -2px; }
.user-menu-list .logout-form { display: block; }

/* Theme toggle (U5): compact glyph button in the header, a header control like
   the page links (IW-530), square at their height. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  height: 28px;
  min-width: 28px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-header-text-rest);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 4px 7px;
}
.theme-toggle:hover {
  color: var(--color-text-inverse);
  background: var(--color-header-control-hover);
}
.theme-toggle-slot { display: inline-flex; align-items: center; }
.theme-toggle svg { width: 16px; height: 16px; display: block; }

/* ── Queue rail ── */
.queue-rail {
  grid-row: 2;
  grid-column: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--color-bg-surface);
  border-right: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  line-height: 1.4;
}

/* Rail header: open-by-number + archive search (redesign U6).
   position: relative anchors the floating search-results panel. */
.rail-header {
  position: relative;
  padding: var(--space-md);
  border-bottom: 1px solid var(--color-border-subtle);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  flex: none;
}
.rail-open-row { display: flex; gap: var(--space-xs); }
.rail-open-row input { flex: 1; min-width: 0; }
/* A ticket number is typed or pasted, never stepped (IW-467), so the browser's
   spinner arrows stay hidden; the field keeps type=number for the numeric
   keypad on a phone. Chromium and WebKit draw the arrows as a pseudo-element,
   Firefox as the control's own appearance, so both are addressed. */
.rail-open-row input[type=number] { appearance: textfield; -moz-appearance: textfield; }
.rail-open-row input[type=number]::-webkit-outer-spin-button,
.rail-open-row input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.rail-open-row .btn-primary { padding: var(--space-sm) var(--space-md); flex: none; }
.rail-header .alert { margin-bottom: 0; }
.shell.rail-collapsed .rail-header { display: none; }

/* Filter and sort chips + popover (IW-49). Two compact pills under the
   search field; the popover floats over the rail list like the search
   results panel. Chips turn accent when the view is narrowed. */
/* The chips wrap onto a second line when the rail is dragged narrow (IW-390);
   the rail clips sideways, so a chip that did not wrap would be cut off. */
.rail-filters { position: relative; display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.rail-fchip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: var(--font-size-xs);
  background: var(--color-bg-surface);
  color: var(--color-text-secondary);
  cursor: pointer;
  white-space: nowrap;
}
.rail-fchip:hover { background: var(--color-bg-hover); }
.rail-fchip.rail-fchip-active { border-color: var(--color-accent); color: var(--color-accent); }
.rail-fchip svg { width: 12px; height: 12px; flex: none; }
.rail-pop {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: var(--space-xs);
  z-index: 21;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-sm);
}
.pop-h {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: var(--space-sm) 0 2px;
}
.pop-h:first-child { margin-top: 0; }
.pop-opt {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 2px 0;
  cursor: pointer;
  color: var(--color-text-secondary);
}
.pop-opt input { accent-color: var(--color-accent); margin: 0; }
/* The State rows of the filter popover carry their legend glyph (IW-389), in
   the row's own state colour via .st-*, one step larger than the legend's
   11px so it holds its own beside a 13px checkbox. Glyph and label sit at the
   legend's 4px, not the option row's gap, and the label keeps the secondary
   text colour: only the glyph is chromatic, as in the list. */
.pop-state { display: inline-flex; align-items: center; gap: 4px; }
.pop-state span { display: inline-flex; flex: none; }
.pop-state svg { width: 12px; height: 12px; }
/* One quiet line of state under a popover's controls, e.g. which way the
   chosen sort runs (IW-106). Indented to sit under the option labels. */
.pop-note {
  margin-top: 2px;
  padding-left: calc(13px + var(--space-sm));
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.pop-reset {
  margin-top: var(--space-sm);
  padding: 0;
  background: none;
  border: none;
  color: var(--color-accent);
  font-size: var(--font-size-xs);
  text-decoration: underline;
  cursor: pointer;
}
.rail-filter-empty {
  padding: var(--space-md);
  color: var(--color-text-muted);
  font-style: italic;
  font-size: var(--font-size-sm);
}

/* Search results: floating panel over the rail list. Anchored below the
   search input; rail rows sit inert beneath it (clicks land on the panel).
   Escape / click-outside dismissal lives in rail.js. */
.rail-search-results {
  display: none;
  position: absolute;
  top: 100%;
  left: var(--space-sm);
  right: var(--space-sm);
  margin-top: var(--space-xs);
  z-index: 20;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
  padding: var(--space-sm) var(--space-md);
  max-height: 60vh;
  overflow-y: auto;
  font-size: var(--font-size-sm);
}
.rail-search-results.open { display: block; }

.rail-list { flex: 1 0 auto; padding: var(--space-xs) 0; }
/* Collapsed rail shows only the expand control (IW-259): the ticket rows
   used to stay laid out at 56px and clip their own text against the edge
   instead of folding away cleanly. */
.shell.rail-collapsed .rail-list { display: none; }

/* Rail states: skeleton (shimmer-free dim blocks), empty, refreshed-at */
.rail-skeleton-row {
  height: 18px;
  margin: 9px var(--space-md);
  border-radius: var(--radius-sm);
  background: var(--color-bg-hover);
}
.rail-empty {
  padding: var(--space-md);
  color: var(--color-text-muted);
  font-style: italic;
}
/* The first queue load failed and there is no list to keep up (IW-577): a
   plain sentence where the bars were, upright rather than italic, since it is
   a fact about the rail and not an empty filter. The footer stamp carries the
   failure colour. */
.rail-no-list {
  margin: 0;
  padding: var(--space-md);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  line-height: 1.45;
}
.rail-footer {
  flex: none;
  padding: var(--space-sm) var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
  position: sticky;
  bottom: 0;
  background: var(--color-bg-surface);
}
.shell.rail-collapsed .rail-footer { display: none; }
.rail-refreshed { font-size: 11px; color: var(--color-text-muted); }
.rail-refreshed-failed { color: var(--color-status-fault); }
.rail-refreshed-busy { color: var(--color-status-warning); }

/* Legend: the icon language, learnable without hovering (IW-48). */
.rail-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding-bottom: var(--space-xs);
  font-size: 10.5px;
  color: var(--color-text-muted);
}
.rail-legend span { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.rail-legend svg { width: 11px; height: 11px; flex: none; }

/* Collapse chevron, pinned at the rail/pane boundary (wired in rail.js, IW-48) */
.rail-collapse-btn {
  position: absolute;
  top: 64px;
  left: calc(var(--spine-column, 0px) + var(--rail-width));
  transform: translateX(-50%);
  z-index: 5;
  width: 20px;
  height: 20px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border-subtle);
  background: var(--color-bg-raised);
  color: var(--color-text-secondary);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
}
.rail-collapse-btn:hover { background: var(--color-bg-hover); }
.shell.shell-no-rail .rail-collapse-btn { display: none; }

/* Resize handle on the same boundary (IW-390). A 10px strip centred on the
   rail's edge is the hit area; what shows is a short grip mark at mid-height,
   quiet in the muted text tone so it reads as part of the edge rather than as
   a control demanding attention. Hover, keyboard focus or a live drag light
   the whole edge in the accent, the one hue reserved for an interactive
   element, so the operator sees exactly which line is about to move. The
   strip sits under the chevron in stacking order: the chevron stays clickable
   and both follow --rail-width, so they track the edge together through a
   drag and through the collapse. The strip starts at the edge and runs into
   the case pane, not across it: the rail scrolls, and on a platform with
   classic scrollbars its bar sits in the rail's last pixels, which a strip
   centred on the edge would cover with a resize cursor. The lit edge and the
   grip overhang the rail by 1px, over its own border only, so they read as
   centred while the hit area stays out of the bar. */
.rail-resize {
  position: absolute;
  top: var(--header-height);
  bottom: var(--spine-row, 0px);
  left: calc(var(--spine-column, 0px) + var(--rail-width));
  z-index: 4;
  width: 10px;
  cursor: col-resize;
  touch-action: none;
}
.rail-resize::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -1px;
  width: 2px;
  background: transparent;
}
.rail-resize::after {
  content: "";
  position: absolute;
  top: 50%;
  left: -1px;
  width: 4px;
  height: 32px;
  margin-top: -16px;
  border-radius: 2px;
  background: var(--color-text-muted);
  opacity: 0.55;
}
.rail-resize:hover::before,
.rail-resize:focus-visible::before,
.shell.rail-resizing .rail-resize::before { background: var(--color-accent); }
.rail-resize:hover::after,
.rail-resize:focus-visible::after,
.shell.rail-resizing .rail-resize::after { background: var(--color-accent); opacity: 1; }
/* Keyboard focus is shown by the lit edge and grip above, not by a ring around
   a strip that has no visible box of its own. */
.rail-resize:focus-visible { outline: none; }
.shell.shell-no-rail .rail-resize { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .rail-resize::before, .rail-resize::after {
    transition: background var(--motion-enter) var(--ease-out), opacity var(--motion-enter) var(--ease-out);
  }
}

/* ── Case pane ── */
/* The pane is the product's one width container (IW-391). A screen that has to
   cope with a narrow window asks the pane how wide IT is, never the window: the
   rail collapses to 56px, expands to 280px and resizes under IW-390, so the same
   window holds panes of very different widths, and a media query on the window
   would fire at the wrong moment in both directions. Every narrow-width rule on
   a screen inside the pane is therefore an @container query on this name, and
   the two steps below are the ones the product has. A later screen picks the
   step it needs rather than adding a third at a width of its own:
     @container pane (max-width: 720px)   narrow: a reading sheds its second half
     @container pane (max-width: 560px)   tight: a reading is tucked away, an
                                          empty badge stops holding its gap, and
                                          a two-column row stacks
   The pane's inline size already comes from the grid track, so inline-size
   containment changes no layout. It also makes the pane the containing block for
   a fixed-position descendant. Nothing renders one inside the pane today; the
   one fixed-position rule in this sheet, .section-drawer, is not used by any
   template or script, and a revival of it belongs outside the pane or it will
   be offset against the pane and clipped by the chat view's overflow. */
.case-pane {
  grid-row: 2;
  grid-column: 2;
  overflow-y: auto;
  min-height: 0;
  padding: 20px;
  container: pane / inline-size;
}
.shell.shell-no-rail .case-pane { grid-column: 1; }
.case-pane-inner { max-width: 960px; margin: 0 auto; }

/* ════════════════════════════════════════════════════════════════════
 * The spine and the section rail (IW-624)
 * ════════════════════════════════════════════════════════════════════
 * Every signed-in page carries the spine, a dark strip of the main pages on
 * the far left, below the header and in the header's own tones, so the two
 * read as one frame around the work. The logo's mark sits centred over it.
 * The spine takes its 56px from the pane, never from the rail.
 *
 * Two custom properties on the shell say where the spine is, and everything
 * that is placed against the rail's edge reads them: --spine-column is the
 * width it takes from the window's left, --spine-row the height it takes from
 * the bottom. rail.js reads --spine-column too, for the drag and the overlay
 * posture. Both default to 0 where no spine is drawn. */
.shell.shell-spine {
  --spine-column: var(--spine-width);
  --spine-row: 0px;
  grid-template-columns: var(--spine-column) var(--rail-width) minmax(0, 1fr);
}
.shell.shell-spine.shell-no-rail { grid-template-columns: var(--spine-column) minmax(0, 1fr); }
.shell.shell-spine.rail-overlay {
  grid-template-columns: var(--spine-column) var(--rail-width-collapsed) minmax(0, 1fr);
}
.shell-spine .queue-rail { grid-column: 2; }
.shell-spine .case-pane { grid-column: 3; }
.shell-spine.shell-no-rail .case-pane { grid-column: 2; }
.shell-spine .site-header { padding-left: calc((var(--spine-width) - 22px) / 2); }

.spine {
  grid-row: 2;
  grid-column: 1;
  position: relative;
  z-index: 6;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 0;
  padding: var(--space-md) 0;
  background: var(--color-bg-inverse);
  color: var(--color-header-text-rest);
}
.spine-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
}
/* A 40px square, the header controls' hover fill, and for the page you are
   on the same fill held with a 3px bar on the spine's own edge, in the inverse
   text tone: the accent stays the header's keyline and the stuck fold badge,
   as it did when these were header links (IW-530). */
.spine-link {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  box-sizing: border-box;
  border-radius: var(--radius-md);
  color: inherit;
  text-decoration: none;
}
.spine-link:hover,
.spine-link[aria-current="page"] {
  color: var(--color-text-inverse);
  background: var(--color-header-control-hover);
}
.spine-link[aria-current="page"]::before {
  content: "";
  position: absolute;
  left: calc((var(--spine-width) - 40px) / -2);
  top: 10px;
  bottom: 10px;
  width: 3px;
  border-radius: 0 2px 2px 0;
  background: var(--color-text-inverse);
}
.spine-icon {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The name is the link's text, so a screen reader reads it at all times; it is
   shown as a tip beside the icon on hover and on keyboard focus. Transparent at
   rest rather than hidden, since hidden would take it out of the reading too. */
.spine-name {
  position: absolute;
  left: calc(100% + var(--space-md));
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  padding: var(--space-xs) var(--space-sm);
  white-space: nowrap;
  background: var(--color-bg-inverse);
  color: var(--color-text-inverse);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-raised);
  font-size: var(--font-size-sm);
  font-weight: 500;
  line-height: 1.35;
  opacity: 0;
  pointer-events: none;
}
.spine-link:hover .spine-name,
.spine-link:focus-visible .spine-name { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .spine-name { transition: opacity var(--motion-enter) var(--ease-out); }
}
.spine-note {
  display: block;
  font-size: var(--font-size-xs);
  font-weight: 400;
  color: var(--color-header-text-rest);
}
/* The count sits on the icon's corner, ringed in the spine's own colour so it
   reads as lying on top of the square rather than inside it. */
.spine-badge {
  position: absolute;
  top: -2px;
  right: -4px;
  min-width: 16px;
  padding: 0 4px;
  line-height: 16px;
  box-shadow: 0 0 0 2px var(--color-bg-inverse);
}

/* The section rail: the page's own sections where the ticket screen has its
   queue. Its rows take the queue's mark for "the one you are on": the hover
   fill held, with the accent's 2px inset edge (.rail-entry-active). */
.section-rail { padding-bottom: var(--space-md); }
.shell.rail-collapsed .section-rail > * { display: none; }
.rail-sections-head { padding: var(--space-lg) var(--space-lg) var(--space-sm); }
.rail-back {
  display: inline-block;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-decoration: none;
}
.rail-back::before { content: "\2039\00a0"; }
.rail-back:hover { color: var(--color-text-primary); }
.rail-sections-title {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: 600;
  line-height: 1.3;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
.rail-sections-list { list-style: none; margin: 0; padding: 0 var(--space-sm); }
.rail-sections-sub { padding: 2px 0 var(--space-xs) var(--space-md); }
.rail-section[hidden] { display: none; }
.rail-section-link {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: 6px var(--space-sm);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  text-decoration: none;
  line-height: 1.35;
}
.rail-section-title { min-width: 0; overflow-wrap: anywhere; }
.rail-section-link:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
.rail-section-link[aria-current] { color: var(--color-text-primary); }
.rail-section-link[aria-current="page"] { font-weight: 600; }
/* The full mark. A rail that is the page's only way between its views
   (.rail-switcher: Learnings' queues, IW-651) gives it to the open view, since
   no tab on the page says it a second time. */
.rail-section-link[aria-current="location"],
.rail-section-link[aria-current="page"]:only-child,
.rail-switcher .rail-section-link[aria-current="page"] {
  background: var(--color-bg-hover);
  box-shadow: inset 2px 0 0 var(--color-accent);
}
.rail-sections-sub .rail-section-link { font-size: var(--font-size-sm); padding-top: 4px; padding-bottom: 4px; }
.rail-section-count {
  margin-left: auto;
  flex: none;
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

/* The finder on Admin settings: one box over every setting in every group,
   its matches floating over the rail list like the queue's search results. */
.rail-finder { position: relative; padding: 0 var(--space-md) var(--space-md); }
.rail-finder-input { width: 100%; box-sizing: border-box; }
.rail-finder-results {
  position: absolute;
  left: var(--space-md);
  right: var(--space-md);
  top: calc(100% - var(--space-md) + var(--space-xs));
  z-index: 6;
  max-height: 60vh;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: var(--space-xs) 0;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
}
.rail-finder-results[hidden] { display: none; }
.rail-finder-hit {
  display: block;
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text-primary);
  text-decoration: none;
}
.rail-finder-hit:hover,
.rail-finder-hit:focus { background: var(--color-bg-hover); }
.rail-finder-hit:focus-visible { outline-offset: -2px; }
.rail-finder-name { display: block; font-size: var(--font-size-sm); }
.rail-finder-where { display: block; font-size: var(--font-size-xs); color: var(--color-text-muted); }
.rail-finder-none {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
/* A section the rail or the finder lands on stops a little below the pane's top
   edge rather than flush against it. */
.case-pane :is(.admin-section, .admin-setting, .learnings-section, .reg-section, .tc-group) {
  scroll-margin-top: var(--space-lg);
}
/* What the finder landed on, ringed for a moment so the eye finds it. */
.admin-setting.is-found,
.admin-section.is-found {
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 2px var(--color-accent);
}

/* On a phone the spine lies along the bottom of the screen, where a thumb
   reaches it, and leaves the width to the work. This is the one window media
   query in the sheet, and the exception is deliberate: the shell is the
   window's own grid, with no box outside it to measure, and making the body a
   size container would make it the containing block of every fixed box on the
   page. The step is the pane's tight one, 560px. The rail keeps its drawer:
   rail.js lays it over the pane on a window this narrow, above the bar. */
@media (max-width: 560px) {
  .shell.shell-spine {
    --spine-column: 0px;
    --spine-row: var(--spine-width);
    grid-template-rows: var(--header-height) minmax(0, 1fr) var(--spine-row);
  }
  .shell-spine .site-header { padding-left: 20px; }
  .spine {
    grid-row: 3;
    grid-column: 1 / -1;
    flex-direction: row;
    justify-content: space-around;
    align-items: center;
    padding: 0 var(--space-sm);
    border-top: 1px solid var(--color-header-divider);
  }
  .spine-list { display: contents; }
  .spine-link[aria-current="page"]::before {
    left: 10px;
    right: 10px;
    top: auto;
    bottom: calc((var(--spine-width) - 40px) / -2);
    width: auto;
    height: 3px;
    border-radius: 2px 2px 0 0;
  }
  .spine-name {
    left: 50%;
    top: auto;
    bottom: calc(100% + var(--space-sm));
    transform: translateX(-50%);
  }
}
@media (forced-colors: active) {
  .spine-link[aria-current="page"]::before {
    forced-color-adjust: none;
    background: CanvasText;
  }
  .spine-link:hover { text-decoration: underline; }
}
.pane-empty {
  padding: 48px 20px;
  text-align: center;
  color: var(--color-text-muted);
}
/* Empty state (IW-50): the blank pane teaches the two fastest paths in. */
.empty-card { max-width: 420px; margin: 10vh auto 0; }
.empty-mark {
  display: inline-block;
  width: 40px;
  height: 40px;
  color: var(--color-accent);
  opacity: 0.85;
  margin-bottom: var(--space-sm);
}
.empty-mark svg { width: 100%; height: 100%; display: block; }
.empty-title {
  font-weight: 600;
  font-size: var(--font-size-md);
  color: var(--color-text-primary);
  margin-bottom: var(--space-xs);
}
.empty-sub { font-size: var(--font-size-sm); line-height: 1.6; }
.empty-sub kbd {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  border: 1px solid var(--color-border-subtle);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  padding: 0 5px;
  color: var(--color-text-secondary);
}
/* The tip (IW-539): one piece of help under the line above, another on request.
   It sits below a hairline as a footnote to the empty state and is set flush
   left, where everything above it is centred: a tip runs to three lines, and
   centred lines of uneven length are hard to read. Its text keeps the size and
   tone of the line above, so the heading stays the loudest thing on the screen.
   The label and Next tip share the row above the text, so the link holds its
   place however long the tip under it runs and a second press lands where the
   first did. */
.empty-tip {
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
  text-align: left;
  font-size: var(--font-size-sm);
  line-height: 1.6;
}
.tip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-xs);
}
.tip-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
/* A link in the hint register, quieter than the tip it sits over. The padding
   is the press target, taken back by the margin so the row keeps its height. */
.tip-next {
  margin: calc(-1 * var(--space-sm)) calc(-1 * var(--space-sm)) calc(-1 * var(--space-sm)) 0;
  padding: var(--space-sm);
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.tip-next:hover { color: var(--color-accent); }
.tip-next[hidden] { display: none; }
.tip-text { margin: 0; }
/* A command reads as the thing typed: the mono face on the code tint. */
.tip-command {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  background: var(--color-bg-code);
  color: var(--color-text-secondary);
  padding: 1px var(--space-xs);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}
/* A prompt is something to say, so it takes the shape of the operator's own
   message in the chat (.msg-user): the same tint, radius and padding, at body
   weight and in the primary tone. Never a heading. */
.tip-say {
  display: inline-block;
  margin: var(--space-sm) 0 0;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  background: var(--color-bg-selected);
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
/* Who shared a team tip (IW-540): a byline under the words to say, in the
   label's quiet register, so the credit is read after the tip and never
   competes with it. Its own line, since the prompt above it is a block. */
.tip-credit {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* ── Session warning banner ── */
.session-warning {
  background: var(--color-status-warning-bg);
  border-bottom: 1px solid var(--color-status-warning);
  padding: 6px 20px;
  font-size: var(--font-size-xs);
  color: var(--color-status-warning);
}

/* ── Claude Code notice (IW-441) ──
 * A model at risk takes the parked banner's warning fill, since a feature may
 * already be failing and everyone should see it. The stack sits above the page it
 * qualifies. On the ticket screen the pane is a full-height flex column, so it is
 * a fixed item there and the chat keeps the rest. */
.cc-notices {
  display: flex;
  flex-direction: column;
  flex: none;
  gap: var(--space-xs);
  margin: 0 0 var(--space-md);
}
.case-pane:has(.chat-workbench) .cc-notices { margin: var(--space-sm) 0 0; }
.cc-notice {
  max-width: var(--measure-prose);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.cc-notice p { margin: 0; }
.cc-notice-lead { font-weight: 600; color: var(--color-text-primary); }
.cc-notice .cc-notice-admin {
  margin-top: var(--space-xs);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
}
.cc-notice-risk {
  background: var(--color-status-warning-bg);
  color: var(--color-text-primary);
}

/* ── Layout ── */
.workbench { display: flex; flex-direction: column; gap: var(--space-lg); }

/* ── Ticket header ── */
.ticket-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
}
.ticket-id { font-weight: 700; font-size: 16px; color: var(--color-text-primary); }
.ticket-subject { font-size: 15px; color: var(--color-text-secondary); }

/* ── Resume card (case state) ── */
/* A plain card: the accent is reserved for selection and interaction, so
   the old accent edge stripe is gone (anomaly-restraint rule). */
.resume-wait {
  font-weight: 400;
  font-size: var(--font-size-xs);
  color: var(--color-status-warning);
  margin-left: var(--space-sm);
}
.resume-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px var(--space-xl);
}
.state-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  font-size: var(--font-size-sm);
  min-width: 0;
}
.state-label {
  font-weight: 600;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}
.state-value { cursor: pointer; border-bottom: 1px dashed var(--color-border-subtle); min-width: 40px; }
.state-value:hover { background: var(--color-bg-hover); }
.state-empty { color: var(--color-text-muted); font-style: italic; }
.state-unsaved { color: var(--color-status-fault); border-bottom-color: var(--color-status-fault); }
.state-editor { flex: 1; max-width: 220px; }
.state-error { font-size: var(--font-size-xs); color: var(--color-status-fault); }

/* ── State proposals (resume card + in-session strip) ── */
.proposal-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin: var(--space-xs) 0 var(--space-sm);
}
/* The standalone strip is its own card; the copy inside the resume card
   stays a bare row (no nested cards). display toggling is app.js's. */
.proposal-strip.card { margin: 0; padding: var(--space-md) var(--space-lg); }
/* Proposal chip: the uniform pill, accent-tinted (a proposal is an
   interaction waiting on the user, not an anomaly). */
.proposal-chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-sm);
  font-size: var(--font-size-xs);
  padding: var(--space-xs) var(--space-md);
  border-radius: 999px;
  background: var(--color-bg-selected);
  border: 1px solid var(--color-border-subtle);
  color: var(--color-text-primary);
}
.proposal-text { font-weight: 500; }
.proposal-spinner { color: var(--color-text-secondary); font-style: italic; }
.proposal-error { color: var(--color-status-fault); }

/* ── Brief sections ── */
details {
  margin-bottom: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}
/* Brief section summaries share the card-header register. Every rule here is
   keyed on the summary's OWN details with the child combinator (IW-416). The
   marker rules were descendant selectors, and `details[open] summary::before`
   matches any summary under any open details, so a closed disclosure inside an
   open one (a help-line fold inside the open About this boat panel) wore the
   open triangle and toggling it changed nothing on screen. Each nested fold
   needed an override of its own until the rule read its own state. The
   combinator adds no specificity, so a class rule that overrides this register
   wins exactly as it did. */
details > summary {
  padding: var(--space-sm) var(--space-md);
  cursor: pointer;
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-secondary);
  background: var(--color-bg-page);
  border-radius: var(--radius-sm);
  list-style: none;
}
/* Transient status text inside a summary stays plain. */
details > summary span {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}
details > summary::-webkit-details-marker { display: none; }
details > summary::before { content: "▶ "; font-size: 10px; }
details[open] > summary::before { content: "▼ "; }

.section-body { padding: var(--space-md); font-size: var(--font-size-sm); }
.section-body.loading { color: var(--color-text-muted); font-style: italic; }

/* ── Dive card (narrative brief U4) ── */
/* The brief's front door: cited claims at a comfortable prose measure under
   the absorbed ticket header (typography registers per U6: claims read as
   prose, chips and drawers keep the denser data register). min-height holds
   the card's footprint from pane open, so the skeleton-to-content swap
   causes no layout shift. */
.dive-card-region { margin-bottom: var(--space-md); }
.dive-card {
  min-height: 180px;
  padding: var(--space-md) var(--space-lg);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}
.dive-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
}
.dive-ticket { min-width: 0; }
.dive-ticket.loading { color: var(--color-text-muted); font-style: italic; }
.dive-ticket-subject {
  font-size: var(--font-size-md);
  font-weight: 600;
  overflow-wrap: anywhere;
}
.dive-ticket-meta {
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}
.dive-controls {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
/* Freshness chip: always visible, recolored through status tokens only
   (the delta-badge honesty convention). Building/regenerating ride the
   waiting tint; stale and failed add a border so the state reads even at
   a glance. */
.dive-state-loading, .dive-state-building, .dive-state-regenerating {
  background: var(--color-status-waiting-bg);
  color: var(--color-status-waiting);
  font-style: italic;
}
.dive-state-current {
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
}
.dive-state-stale {
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning);
  border: 1px solid var(--color-status-warning);
}
.dive-state-unknown {
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning);
}
.dive-state-failed {
  background: var(--color-status-fault-bg);
  color: var(--color-status-fault);
  border: 1px solid var(--color-status-fault);
}
/* Skeleton: fixed-shape placeholder lines, visibly not content. */
.dive-skeleton-lines { display: grid; gap: var(--space-sm); }
.dive-skeleton-line {
  height: 14px;
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
}
.dive-skeleton-line-short { width: 60%; }
/* Regenerating: the prior dive stays readable under the marker, dimmed. */
.dive-card.dive-regenerating .dive-body { opacity: 0.6; }
/* Claims: the dive's facts, plain prose at a comfortable reading register
 * (U6): ~68ch measure, 1.55 line height, regular weight. overflow-wrap
 * guards hostile unbroken runs (120-char subjects, 300-char URLs) routed
 * through a claim; the stored text is never shortened. */
.dive-claims { max-width: 68ch; }
.dive-claim {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-md);
  font-weight: 400;
  line-height: 1.55;
  overflow-wrap: anywhere;
  min-width: 0;
}
/* Citation affordance: superscript-style chip after the claim text. */
.dive-cite {
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  background: var(--color-bg-code);
  color: var(--color-text-secondary);
  font-size: 10px;
  font-weight: 600;
  line-height: 1.4;
  padding: 0 var(--space-xs);
  vertical-align: super;
  cursor: pointer;
}
.dive-cite:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
/* Suggestions: a visually distinct, clearly machine-marked module. The AI
   marker chip plus its own tinted-border surface keep model proposals
   distinguishable from the claims at a glance. Read-only. */
.dive-suggestions {
  margin-top: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  max-width: 68ch;
  border-left: 3px solid var(--color-status-waiting);
  background: var(--color-bg-page);
  border-radius: var(--radius-sm);
}
.dive-suggestions-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-sm);
}
.dive-ai-marker {
  background: var(--color-status-waiting-bg);
  color: var(--color-status-waiting);
}
.dive-suggestion { margin-bottom: var(--space-sm); min-width: 0; }
.dive-suggestion-text {
  font-size: var(--font-size-sm);
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.dive-reasoning-toggle { margin-top: var(--space-xs); }
.dive-reasoning {
  margin-top: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  background: var(--color-bg-code);
  border-radius: var(--radius-sm);
}
.dive-failed-note { color: var(--color-text-secondary); }

/* ── Brief chip strip (narrative brief U3) ── */
/* Auxiliary sections render as chips; anomaly tints recolor through the
   shared badge-* modifiers. The strip wraps to additional rows at narrow
   widths, never overflows. */
.section-chip-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}
/* Chips are real buttons riding the shared pill shape (grouped .chip rule);
   reset the button chrome on top of it. */
.section-chip {
  border: 1px solid var(--color-border-subtle);
  cursor: pointer;
  font-family: inherit;
  padding: var(--space-xs) var(--space-md);
  max-width: 100%;
  min-width: 0;
}
.section-chip:hover { background: var(--color-bg-hover); }
/* Gist: dense data register, CSS-only truncation. The chip keeps the pill's
 * nowrap; an oversized gist (hostile 120-char subject) ellipsizes here while
 * applySectionState mirrors the full text into the chip's title attribute. */
.section-chip .chip-gist {
  font-weight: 400;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Loading state: italic, muted, distinct from the empty state in the drawer. */
.section-chip.chip-loading { color: var(--color-text-muted); font-style: italic; }
/* Nominal sections render muted; anomalous chips recolor via badge-warning /
   badge-waiting (tint background + matching text). */
.section-chip.chip-nominal { color: var(--color-text-muted); }
/* Failed (stored copy incomplete): the chip is the retry affordance. */
.section-chip.chip-failed { border-style: dashed; border-color: var(--color-status-warning); }

/* ── Section drawer (narrative brief U3) ── */
/* One drawer: full pane height, internal scroll, overlays the case pane
   from the right. Raised-surface treatment like the search panel. */
.section-drawer {
  display: none;
  position: fixed;
  top: var(--header-height);
  right: 0;
  bottom: 0;
  width: min(440px, 85vw);
  z-index: 30;
  flex-direction: column;
  background: var(--color-bg-raised);
  border-left: 1px solid var(--color-border-subtle);
  box-shadow: var(--shadow-raised);
}
.section-drawer.open { display: flex; }
.drawer-header {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
}
.drawer-header .card-header { margin: 0; }
.drawer-close { flex: none; }
.drawer-body {
  flex: 1;
  min-height: 0;
  min-width: 0;
  overflow-y: auto;
  padding: var(--space-md) var(--space-lg);
  font-size: var(--font-size-sm);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
/* Loading vs empty: loading is italic muted, the empty state is upright
   secondary text under its section heading. Visually distinct. */
.drawer-loading { color: var(--color-text-muted); font-style: italic; }
.drawer-empty { color: var(--color-text-secondary); }
.drawer-empty-heading {
  display: block;
  font-weight: 600;
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-xs);
}
.drawer-note { color: var(--color-text-secondary); }

/* ── Decoded faults (drawer content) ── */
.fault-entry {
  max-width: 72ch;
  margin-bottom: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-bg-page);
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
  min-width: 0;
}
.fault-matched {
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* ── Conversations ── */
/* Bordered message blocks at a readable measure; metadata line above the
   body. Calm: borders, not zebra fills. */
.conv-entry {
  max-width: 72ch;
  margin-bottom: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}
.conv-meta { font-size: var(--font-size-xs); color: var(--color-text-muted); margin-bottom: var(--space-xs); }
.conv-date { margin-left: var(--space-sm); }
/* Bodies wrap hostile unbroken runs (300-char URLs) in place; the show-more
 * remainder span (U5) inherits the same guard, so expansion never overruns. */
.conv-body {
  white-space: pre-wrap;
  font-size: var(--font-size-md);
  line-height: 1.5;
  overflow-wrap: anywhere;
  min-width: 0;
}
/* Delta anomalies: the only chromatic message blocks. */
.conv-new {
  background: var(--color-status-resolved-bg);
  border-color: var(--color-status-resolved);
}
.conv-edited {
  background: var(--color-status-warning-bg);
  border-color: var(--color-status-warning);
}
.conv-flag { text-transform: uppercase; }
/* Show-earlier header (U5): quiet full-width toggle above the thread. The
   badge surfaces new/edited counts hidden inside the collapsed region. */
.conv-show-earlier {
  display: block;
  width: 100%;
  max-width: 72ch;
  margin-bottom: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  background: none;
  border: 1px dashed var(--color-border-subtle);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  cursor: pointer;
}
.conv-show-earlier:hover {
  color: var(--color-text-secondary);
  background: var(--color-bg-hover);
}
.conv-earlier-badge {
  display: inline-flex;
  margin-left: var(--space-sm);
  padding: 1px var(--space-sm);
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning);
}
/* Per-message show-more (U5): the body cap is display-only; the hidden
   remainder span carries the rest of the stored text. */
.conv-show-more { margin-top: var(--space-xs); }
.conv-flag-new { background: var(--color-status-resolved-bg); color: var(--color-status-resolved); }
.conv-flag-edited { background: var(--color-status-warning-bg); color: var(--color-status-warning); }

/* ── Delta badge (fetch state after replay) ── */
/* Pill shape from the badge base; states recolor via status tokens only. */
.delta-badge { cursor: pointer; }
.delta-fetching, .delta-current { background: var(--color-status-waiting-bg); color: var(--color-status-waiting); }
.delta-has-new {
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
  border: 1px solid var(--color-status-resolved);
}
.delta-failed {
  background: var(--color-status-fault-bg);
  color: var(--color-status-fault);
  border: 1px solid var(--color-status-fault);
}

/* Brief panel showing stored content after a failed delta fetch */
.panel.stale { outline: 1px dashed var(--color-status-fault); }

/* ── Rebuild-needed marker (failed/pending stored section) ── */
/* Generation status is an anomaly: warning tint block. */
.rebuild-needed {
  color: var(--color-status-warning);
  font-size: var(--font-size-sm);
  background: var(--color-status-warning-bg);
  border: 1px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
}
.rebuild-note { font-size: var(--font-size-xs); color: var(--color-text-muted); }

/* ── Image / similar ── */
.img-entry, .similar-entry {
  max-width: 72ch;
  margin-bottom: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-bg-page);
  border-radius: var(--radius-sm);
}
.similar-reply { margin-top: var(--space-xs); color: var(--color-text-secondary); }

/* ── Hardware warnings ── */
/* Warning tint ONLY when warnings exist (app.js adds .hw-warnings to the
   list); the empty state stays a neutral line. */
.hw-warnings {
  margin: 0;
  padding: var(--space-sm) var(--space-md) var(--space-sm) 28px;
  background: var(--color-status-warning-bg);
  border: 1px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  color: var(--color-status-warning);
}

/* ── Chat ── */
.chat-history { min-height: 60px; max-height: 300px; overflow-y: auto; margin-bottom: var(--space-md); }
.chat-turn {
  max-width: 72ch;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-md);
  line-height: 1.5;
}
.chat-turn .chat-meta, .chat-turn strong { font-size: var(--font-size-xs); }
.chat-turn.user strong { color: var(--color-text-primary); }
.chat-turn.assistant strong { color: var(--color-text-secondary); }
.chat-input-row { display: flex; gap: var(--space-sm); }
.chat-input-row input { flex: 1; }

/* ── Draft / note textareas ── */
.draft-textarea {
  width: 100%;
  min-height: 140px;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--font-size-sm);
  resize: vertical;
  margin-bottom: var(--space-sm);
}
.draft-controls { margin-bottom: var(--space-sm); display: flex; gap: var(--space-sm); flex-wrap: wrap; }
.draft-actions { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.streaming-indicator { font-size: var(--font-size-xs); color: var(--color-text-muted); margin-bottom: 6px; }
.copy-confirm { font-size: var(--font-size-xs); color: var(--color-status-resolved); }
.copy-fallback { font-size: var(--font-size-xs); color: var(--color-text-muted); margin-top: var(--space-sm); }
.note-status { font-size: var(--font-size-xs); color: var(--color-text-secondary); }

/* ── RMA ── */
.rma-result {
  margin-top: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-status-resolved-bg);
  border-radius: var(--radius-sm);
}
.rma-result a { color: var(--color-accent); }
/* The open card a click did not file over (IW-717), linked from the status line. */
.rma-status a { color: var(--color-accent); }

/* ── Forms ── */
.field { margin-bottom: var(--space-md); }
/* Explanatory text under an input, on the standalone login/setup cards where
   there is no console panel to carry an .admin-note (IW-128). */
.field-hint {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
}
.field-hint code { word-break: break-all; }
.field label {
  display: block;
  font-size: var(--font-size-xs);
  font-weight: 600;
  margin-bottom: var(--space-xs);
  color: var(--color-text-secondary);
}
input[type=text], input[type=number], input[type=password], input[type=search],
textarea, select {
  width: 100%;
  padding: 7px 9px;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--font-size-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
}
input:focus, textarea:focus, select:focus { outline: 2px solid var(--color-accent); border-color: transparent; }

/* ── Buttons ── */
.btn-primary {
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  border: none;
  padding: var(--space-sm) 18px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--font-size-sm);
  font-weight: 500;
}
.btn-primary:hover { background: var(--color-accent-hover); }
/* Send flips to Stop while a turn runs: an amber, quieter affordance to interrupt. */
.btn-primary.btn-stop { background: var(--color-status-warning); color: var(--color-text-on-accent); }
.btn-primary.btn-stop:hover { filter: brightness(0.95); }
.btn-secondary {
  background: var(--color-bg-page);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-subtle);
  padding: 7px var(--space-lg);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--font-size-sm);
}
.btn-secondary:hover { background: var(--color-bg-hover); }
/* The quiet bordered button already wears the subtle border and the page fill, so
 * like the outline button it has no colour to give up: the label steps down to
 * muted and the fill is pinned so hover cannot lift it. Ties the hover rule on
 * specificity and beats it on order, so it stays directly below it (IW-367). */
.btn-secondary:disabled {
  background: var(--color-bg-page);
  color: var(--color-text-muted);
  cursor: default;
}
.btn-danger {
  background: var(--color-status-fault);
  color: var(--color-text-on-status);
  border: none;
  padding: var(--space-sm) 18px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--font-size-sm);
}
.btn-danger:hover { filter: brightness(0.9); }
/* A disabled filled button gives up its colour: the fill drops to the border tone
 * and the label to muted, so it cannot be mistaken for a live one. Each selector
 * ties the hover or stop rule it overrides on specificity and beats it on order, so
 * this block stays below them. The composer disables its button while it is a Stop,
 * and .btn-primary.btn-stop:hover carries three classes, hence the third selector. */
.btn-primary:disabled,
.btn-primary.btn-stop:disabled,
.btn-danger:disabled {
  background: var(--color-border-subtle);
  color: var(--color-text-muted);
  cursor: default;
  filter: none;
}
.btn-small {
  background: none;
  border: 1px solid var(--color-text-muted);
  color: var(--color-text-primary);
  padding: 2px var(--space-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--font-size-xs);
}
.btn-small:hover { background: var(--color-bg-hover); }
/* The outline button has no fill to give up, so its border and label step down instead. */
.btn-small:disabled {
  background: none;
  border-color: var(--color-border-subtle);
  color: var(--color-text-muted);
  cursor: default;
}
.btn-link {
  background: none;
  border: none;
  color: var(--color-text-inverse);
  opacity: 0.7;
  cursor: pointer;
  font-size: var(--font-size-sm);
  padding: 0;
  text-decoration: underline;
}
.btn-link:hover { opacity: 1; }

/* ── Alerts ── */
.alert {
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-md);
  font-size: var(--font-size-sm);
}
.alert-error {
  background: var(--color-status-fault-bg);
  color: var(--color-status-fault);
  border: 1px solid var(--color-status-fault);
}
.alert-warn {
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning);
  border: 1px solid var(--color-status-warning);
}
/* Success flash (settings consoles: "credential saved", "setting saved"). */
.alert-ok {
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
  border: 1px solid var(--color-status-resolved);
}

/* ── Login page ── */
/* Bebas Neue lives on this page only (IW-50): the display face for the brand
   lockup over the charcoal ground; the working UI stays IBM Plex. */
@font-face {
  font-family: "Bebas Neue";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/vendor/fonts/bebas-neue-400.woff2") format("woff2");
}
.login-page body, body.login-page { background: var(--color-bg-inverse); }
.login-lockup { text-align: center; margin: 12vh auto 28px; }
.login-mark {
  display: inline-block;
  width: 72px;
  height: 72px;
  color: var(--color-accent);
  margin-bottom: var(--space-md);
}
.login-mark svg { width: 100%; height: 100%; display: block; }
.login-wordmark {
  font-family: "Bebas Neue", var(--font-sans);
  font-size: 38px;
  letter-spacing: 0.04em;
  line-height: 1;
  color: var(--color-text-inverse);
}
.login-card {
  max-width: 380px;
  margin: 0 auto 60px;
  background: var(--color-bg-surface);
  padding: 32px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* The Atlassian button leads; the password form follows below a quiet rule for the
 * accounts that use one (IW-623). Both actions span the card, so the two doors
 * read as a choice of the same weight in place, with the button first. */
.login-card .login-oauth {
  display: block;
  text-align: center;
  text-decoration: none;
  padding: 10px 18px;
}
.login-card form button[type="submit"] { width: 100%; }
.login-divider {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin: var(--space-lg) 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.login-divider::before, .login-divider::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--color-border-subtle);
}
/* Choose a new password (IW-623 R12): the second half of signing in, on the same card. */
.login-title {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-primary);
}
.login-lede {
  margin: 0 0 var(--space-lg);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.login-hint {
  margin: calc(-1 * var(--space-sm)) 0 var(--space-md);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.login-card .login-alt { margin-top: var(--space-md); text-align: center; }
.login-card .login-alt button[type="submit"] {
  width: auto;
  color: var(--color-text-secondary);
  opacity: 1;
}

/* ── Get started (IW-956) ─────────────────────────────────────────────────────
 * A new person's five chapters, one per screen (web/templates/get_started.html).
 * The page continues the login page above: its charcoal ground, the mark, and the
 * Bebas Neue display face, which this page and the login page alone use. What
 * Integra reads is drawn as a library: the sources are books on a shelf, grouped by
 * who put them there, and the Claude seat is the reading lamp. A saved key adds its
 * books or lights the lamp, and that change is the page's one moment of motion.
 * Semantic tokens only; nothing here changes the shared layer. */
body.gs-page { background: var(--color-bg-inverse); }
.gs-page [hidden] { display: none !important; }
.gs button { font-family: inherit; }
.gs-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
/* The page is its own width container, as the case pane is on the ticket screen,
 * and it narrows at the product's tight step rather than at a window width. */
.gs { container: gs / inline-size; min-height: 100dvh; font-family: var(--font-sans); color: var(--color-text-inverse); }
.gs-wrap { max-width: 640px; margin: 0 auto; padding: 0 var(--space-lg) 56px; }

/* The thread: the chapters by name, where you are, and the way back. A finished
 * chapter is a link; there is no step count and no progress bar. */
.gs-thread { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px 28px; padding: 22px var(--space-lg) 0; font-size: var(--font-size-sm); }
.gs-th { display: inline-flex; align-items: center; gap: var(--space-sm); color: var(--color-header-text-rest); text-decoration: none; opacity: 0.55; }
.gs-th-dot { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--color-header-divider); box-sizing: border-box; }
.gs-th.is-done { opacity: 1; }
.gs-th.is-done .gs-th-dot { background: var(--color-accent); border-color: var(--color-accent); }
.gs-th.is-now { opacity: 1; color: var(--color-text-inverse); font-weight: 600; }
.gs-th.is-now .gs-th-dot { background: var(--color-text-inverse); border-color: var(--color-text-inverse); }
a.gs-th:hover { color: var(--color-text-inverse); text-decoration: underline; text-underline-offset: 3px; }

/* The top of a chapter: the mark on the first and last, a headline, one paragraph. */
.gs-top { text-align: center; padding-top: 8vh; }
.gs-top.is-inner { padding-top: 5vh; }
.gs-mark { display: inline-block; width: 64px; height: 64px; color: var(--color-header-divider); margin-bottom: var(--space-lg); }
.gs-mark svg { width: 100%; height: 100%; display: block; }
.gs.is-live .gs-mark { color: var(--color-accent); }
.gs-h { margin: 0; font-family: "Bebas Neue", var(--font-sans); font-weight: 400; font-size: 44px; letter-spacing: 0.03em; line-height: 1; color: var(--color-text-inverse); }
.gs-h.is-inner { font-size: 32px; }
.gs-lede { margin: var(--space-md) auto 0; max-width: 54ch; font-size: 15px; line-height: 1.55; color: var(--color-header-text-rest); text-wrap: pretty; }
/* How the assistant reads the library, under the shelf on the library chapter. */
.gs-lede.gs-reads { margin-top: 36px; text-align: center; }

/* Back and the one next action, together under the content. */
.gs-nav { display: flex; align-items: center; justify-content: center; gap: var(--space-xl); margin-top: 36px; }
.gs-nav .gs-go { display: inline-block; font-size: 15px; padding: 10px 24px; text-decoration: none; }
.gs-back { color: var(--color-header-text-rest); text-decoration: none; font-size: var(--font-size-md); }
.gs-back:hover { color: var(--color-text-inverse); text-decoration: underline; text-underline-offset: 3px; }
.gs-foot { text-align: center; margin: 36px auto 0; max-width: 48ch; font-size: var(--font-size-sm); line-height: 1.5; color: var(--color-header-text-rest); opacity: 0.8; }
.gs-foot a { color: inherit; text-underline-offset: 3px; }
/* The one thing to have before starting: the admin's Claude invitation, said on the welcome. */
.gs-before { text-align: center; margin: 28px auto 0; max-width: 50ch; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-header-text-rest); }
.gs-before strong { color: var(--color-text-inverse); font-weight: 600; }
.gs-th:focus-visible, .gs-back:focus-visible, .gs-go:focus-visible, .gs-foot a:focus-visible, .gs-group-cap a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; border-radius: 2px; }

/* The library. Each group of books stands on its own plank, so on a phone the
 * groups wrap and each keeps its plank. A book still to be added is an outline. */
.gs-lib { width: min(880px, calc(100vw - 2 * var(--space-lg))); margin: 36px 0 0 50%; transform: translateX(-50%); }
.gs-shelf { display: flex; justify-content: center; align-items: flex-end; flex-wrap: wrap; gap: 24px 28px; }
.gs-group { display: grid; justify-items: center; gap: 10px; }
.gs-books { display: flex; align-items: flex-end; gap: 4px; padding: 0 var(--space-sm); border-bottom: 3px solid var(--color-header-divider); border-radius: 0 0 1px 1px; }
.gs-spine {
  display: flex; align-items: center; justify-content: center; box-sizing: border-box;
  width: 34px; min-height: 118px; padding: 14px 0 12px;
  writing-mode: vertical-rl; font-size: 12px; font-weight: 500; line-height: 1; letter-spacing: 0.01em; white-space: nowrap;
  border-radius: 3px 3px 0 0;
  border: 1px solid var(--color-header-divider); border-bottom: 0; border-top: 5px solid var(--color-header-divider);
  color: var(--color-header-text-rest); opacity: 0.6;
}
.gs-spine:nth-child(2n) { width: 38px; }
.gs-group.is-lit .gs-spine { opacity: 1; color: var(--color-text-inverse); background: var(--color-header-control-hover); border-color: var(--color-header-control-hover); border-top-color: var(--color-accent); }
.gs-group-cap { font-size: var(--font-size-xs); line-height: 1.4; color: var(--color-header-text-rest); text-align: center; max-width: 176px; min-height: 2.8em; text-wrap: balance; opacity: 0.8; }
.gs-group.is-lit .gs-group-cap { opacity: 1; }
.gs-group-cap a { color: var(--color-text-inverse); text-underline-offset: 2px; }

/* The lamp: dark until the seat connects, then the shade takes the accent and throws
 * a cone of light down over its plank. */
.gs-lamp { width: 72px; height: 128px; display: flex; align-items: flex-end; justify-content: center; padding: 0 var(--space-sm); border-bottom: 3px solid var(--color-header-divider); }
.gs-lamp svg { width: 56px; height: 128px; display: block; overflow: visible; }
.gs-lamp .lamp-shade { fill: var(--color-header-control-hover); stroke: var(--color-header-divider); stroke-width: 1.5; }
.gs-lamp .lamp-stem, .gs-lamp .lamp-base { fill: var(--color-header-divider); }
.gs-lamp .lamp-glow { fill: url(#gs-glow); opacity: 0; }
.gs-group.is-lit .lamp-shade { fill: var(--color-accent); stroke: var(--color-accent); }
.gs-group.is-lit .lamp-stem, .gs-group.is-lit .lamp-base { fill: var(--color-header-text-rest); }
.gs-group.is-lit .lamp-glow { opacity: 1; }
.gs-glow-stop-a { stop-color: var(--color-accent); stop-opacity: 0.45; }
.gs-glow-stop-b { stop-color: var(--color-accent); stop-opacity: 0; }

/* A key chapter's own piece of the library, at the top of its card: the two books the
 * Freshdesk key adds, or the lamp. Drawn in the card's tones rather than the ground's. */
.gs-piece { display: flex; align-items: center; gap: var(--space-lg); margin: 0 0 var(--space-xl); padding-bottom: var(--space-xl); border-bottom: 1px solid var(--color-border-subtle); }
.gs-piece .gs-books { gap: 3px; padding: 0 6px; border-bottom-color: var(--color-border-subtle); }
.gs-piece .gs-spine { width: 30px; min-height: 96px; font-size: 11.5px; padding: 12px 0 10px; border-color: var(--color-border-subtle); border-top-color: var(--color-border-subtle); color: var(--color-text-muted); opacity: 1; }
.gs-piece .gs-spine:nth-child(2n) { width: 32px; }
.gs-piece .gs-group.is-lit .gs-spine { background: var(--color-bg-inverse); border-color: var(--color-bg-inverse); border-top-color: var(--color-accent); color: var(--color-text-inverse); }
.gs-piece .gs-lamp { width: 60px; height: 100px; border-bottom-color: var(--color-border-subtle); }
.gs-piece .gs-lamp svg { width: 44px; height: 100px; }
.gs-piece .gs-lamp .lamp-shade { fill: var(--color-bg-page); stroke: var(--color-border-subtle); }
.gs-piece .gs-lamp .lamp-stem, .gs-piece .gs-lamp .lamp-base { fill: var(--color-border-subtle); }
.gs-piece .gs-group.is-lit .lamp-shade { fill: var(--color-accent); stroke: var(--color-accent); }
.gs-piece .gs-group.is-lit .lamp-stem, .gs-piece .gs-group.is-lit .lamp-base { fill: var(--color-text-muted); }
.gs-piece-text { margin: 0; font-size: var(--font-size-md); line-height: 1.5; color: var(--color-text-secondary); max-width: 40ch; }
.gs-piece-text b { color: var(--color-text-primary); font-weight: 600; }

/* The card: one form on the ground, like the login card. */
.gs-card { max-width: 520px; margin: 32px auto 0; background: var(--color-bg-raised); color: var(--color-text-primary); border-radius: 8px; padding: 28px; box-shadow: var(--shadow-raised); text-align: left; }
.gs-card :where(p) { margin: 0; }
.gs-help a, .gs-card-note a { color: var(--color-accent); }
.gs-help { margin: var(--space-md) 0 var(--space-lg); font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-text-secondary); max-width: 60ch; }
.gs-out { display: inline-flex; align-items: center; gap: var(--space-sm); height: 36px; box-sizing: border-box; text-decoration: none; }
.gs-out svg { width: 14px; height: 14px; }
.gs-paste { display: flex; gap: var(--space-sm); }
.gs-paste input { flex: 1; min-width: 0; width: auto; height: 36px; font-size: var(--font-size-md); box-sizing: border-box; }
.gs-paste .btn-primary { height: 36px; white-space: nowrap; }
.gs-paste.is-refused input { border-color: var(--color-status-fault); }
.gs-refused { font-size: var(--font-size-sm); line-height: 1.5; color: var(--color-status-fault); }
.gs-card .gs-refused:not(:empty) { margin-top: var(--space-md); }
.gs-named { display: flex; align-items: center; gap: var(--space-md); font-size: 15px; line-height: 1.4; color: var(--color-text-primary); }
.gs-tick { width: 26px; height: 26px; border-radius: 50%; background: var(--color-accent); color: var(--color-text-on-accent); display: grid; place-items: center; flex: none; }
.gs-tick svg { width: 14px; height: 14px; }
.gs-card .gs-card-note { margin-top: var(--space-lg); font-size: var(--font-size-sm); color: var(--color-text-muted); }

/* The Claude seat, for someone who has never opened a terminal: the computer picked
 * from the browser with a visible switch, keys drawn as keycaps, one command to copy. */
.gs-os { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); margin: 0 0 var(--space-lg); }
.gs-os-btn { font: inherit; font-size: var(--font-size-sm); font-weight: 500; cursor: pointer; padding: 5px var(--space-lg); border: 1px solid var(--color-border-subtle); background: var(--color-bg-page); color: var(--color-text-secondary); }
.gs-os-btn:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.gs-os-btn + .gs-os-btn { border-left: 0; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; margin-left: calc(-1 * var(--space-sm)); }
.gs-os-btn[aria-pressed="true"] { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-text-on-accent); }
.gs-os-note { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.gs-key { display: inline; font-family: var(--font-sans); font-size: 12px; font-weight: 500; line-height: 1.4; padding: 0 6px; margin: 0 1px; border: 1px solid var(--color-border-subtle); border-bottom-width: 2px; border-radius: var(--radius-sm); background: var(--color-bg-surface); color: var(--color-text-primary); white-space: nowrap; }
.gs-how { margin: 0 0 var(--space-lg); padding: 0; list-style: none; counter-reset: how; }
.gs-how li { counter-increment: how; position: relative; padding-left: 28px; margin-bottom: var(--space-lg); font-size: var(--font-size-sm); line-height: 1.6; color: var(--color-text-secondary); }
.gs-how li::before { content: counter(how); position: absolute; left: 0; top: 1px; width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 600; background: var(--color-bg-code); color: var(--color-text-secondary); }
.gs-how li strong { color: var(--color-text-primary); font-weight: 600; }
.gs-how-sub { display: block; margin-top: var(--space-sm); }
.gs-cmd { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--space-sm); margin-top: var(--space-sm); }
.gs-cmd code { font-family: var(--font-mono); font-size: var(--font-size-sm); line-height: 1.5; overflow-wrap: anywhere; background: var(--color-bg-inverse); color: var(--color-text-inverse); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); }
.gs-copy { min-width: 76px; }
.gs-copy.is-copied { border-color: var(--color-accent); color: var(--color-accent); }
.gs-trouble { margin: var(--space-lg) 0 0; border: none; }
.gs-trouble summary { padding: 0; background: none; text-transform: none; letter-spacing: normal; font-size: var(--font-size-sm); font-weight: 500; color: var(--color-accent); cursor: pointer; }
.gs-trouble summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.gs-trouble-list { margin: var(--space-md) 0 0; padding-left: var(--space-lg); font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-text-secondary); }
.gs-trouble-list li { margin-bottom: var(--space-md); }
.gs-elsewhere { border-left: 3px solid var(--color-accent); padding: var(--space-xs) 0 var(--space-xs) var(--space-lg); }
.gs-elsewhere p { margin: 0 0 var(--space-sm); font-size: 15px; line-height: 1.5; }
.gs-elsewhere .gs-help { margin: 0 0 var(--space-md); }
/* The Claude account comes from an admin's invitation, so the chapter says so before any step. */
.gs-invite { margin: 0 0 var(--space-lg); font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-text-secondary); }
.gs-invite strong { color: var(--color-text-primary); }
.gs-elsewhere .gs-show-steps { color: var(--color-accent); opacity: 1; }

/* The finish. */
.gs-ready { text-align: center; margin-top: 32px; }
.gs-waiting { margin: 0 auto; max-width: 44ch; font-size: 15px; line-height: 1.5; color: var(--color-text-inverse); }

@container gs (max-width: 560px) {
  .gs-wrap { padding-bottom: 40px; }
  .gs-thread { gap: var(--space-sm) 14px; padding-top: 16px; }
  .gs-th:not(.is-now) .gs-th-name { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .gs-top { padding-top: 6vh; }
  .gs-h { font-size: 36px; }
  .gs-h.is-inner { font-size: 28px; }
  .gs-shelf { gap: 18px 20px; }
  .gs-spine { width: 30px; font-size: 11.5px; }
  .gs-spine:nth-child(2n) { width: 33px; }
  .gs-card { padding: 20px 16px; }
  .gs-paste { flex-direction: column; }
  .gs-paste .btn-primary { width: 100%; }
  .gs-nav { flex-direction: column-reverse; gap: var(--space-lg); }
  .gs-nav .gs-go { display: block; width: 100%; box-sizing: border-box; text-align: center; }
  .gs-cmd { grid-template-columns: minmax(0, 1fr); }
  .gs-cmd .gs-copy { justify-self: start; }
  .gs-piece { align-items: flex-start; }
}

/* The one moment of motion: books filling in and the lamp lighting when a key is
 * saved. A transition runs only on that change, never on load. */
@media (prefers-reduced-motion: no-preference) {
  .gs-spine, .gs-lamp .lamp-shade, .gs-lamp .lamp-glow, .gs-lamp .lamp-stem, .gs-th-dot {
    transition: background 400ms var(--ease-out), fill 400ms var(--ease-out), opacity 400ms var(--ease-out),
                color 400ms var(--ease-out), border-color 400ms var(--ease-out);
  }
}
/* ── end of Get started (IW-956) ── */

/* ── Get started, Your Jira connection (IW-983) ───────────────────────────────
 * The Jira chapter reuses the Freshdesk chapter's card, piece and steps. What is
 * its own: the scopes to keep, shown as the exact names Atlassian's list uses so
 * they can be matched by eye, and a two field form (email above token). */
.gs-jira-connect { margin-bottom: var(--space-lg); }
.gs-help.gs-help-lead { margin-top: 0; }
.gs-jira-token { margin: 0 0 var(--space-lg); }
.gs-jira-token[open] > summary { margin-bottom: var(--space-lg); }
.gs-how .gs-how-out { display: flex; width: fit-content; margin-top: var(--space-sm); font-size: var(--font-size-sm); height: 32px; }
.gs-scopes { list-style: none; margin: var(--space-sm) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.gs-how .gs-scopes li { counter-increment: none; padding: 0; margin: 0; }
.gs-how .gs-scopes li::before { content: none; }
.gs-scopes code { display: inline-block; font-family: var(--font-mono); font-size: 12px; line-height: 1.5; padding: 1px 8px; border-radius: var(--radius-sm); background: var(--color-bg-page); border: 1px solid var(--color-border-subtle); color: var(--color-text-primary); }
.gs-jira-paste { display: grid; gap: var(--space-sm); }
.gs-label { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-primary); }
.gs-label + input, .gs-label + .gs-paste { margin-bottom: var(--space-sm); }
.gs-jira-paste.is-refused #gs-jira-token { border-color: var(--color-status-fault); }
.gs-jira-paste.is-email-refused #gs-jira-email { border-color: var(--color-status-fault); }
/* The shared input rule names no email type, so the email field takes its look here. */
.gs-jira-email { height: 36px; width: 100%; box-sizing: border-box; padding: 7px 9px; font-family: inherit; font-size: var(--font-size-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); background: var(--color-bg-surface); color: var(--color-text-primary); }
/* ── end of Get started, Your Jira connection (IW-983) ── */

/* ── Archive search results (rendered inside the rail panel) ── */
.search-hit {
  padding: 10px 0;
  border-top: 1px solid var(--color-border-subtle);
}
.search-hit:first-child { border-top: none; }
.search-ticket-link { font-weight: 600; text-decoration: none; color: var(--color-accent); }
.search-ticket-link:hover { text-decoration: underline; }
.search-match { margin: var(--space-xs) 0 0 var(--space-md); color: var(--color-text-secondary); }
.search-type {
  display: inline-block;
  background: var(--color-bg-code);
  color: var(--color-text-secondary);
  border-radius: 3px;
  padding: 1px 6px;
  margin-right: 6px;
  font-size: 11px;
}
.search-snippet { color: var(--color-text-secondary); }

/* ── Requester recall (session page) ── */
.requester-cases {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-md);
}
.requester-cases a { font-weight: 600; text-decoration: none; color: var(--color-accent); }
.requester-cases a:hover { text-decoration: underline; }

/* ── Chat-primary shell (U5) ─────────────────────────────────────────────── */
/* Chat is the one full-height view: the pane itself must not scroll (the transcript does),
   so the composer can pin to the bottom like Claude.ai. Scoped with :has() so every other
   case-pane view keeps its normal scrolling behavior. */
.case-pane:has(.chat-workbench) {
  overflow: hidden;
  padding: 0;
}
.case-pane:has(.chat-workbench) .case-pane-inner {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: 0 20px;
}

.chat-workbench {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
/* The ticket header (IW-606): two lines. The boat's name is the title line and the
   one place the header spends emphasis; the ticket line under it, the number, the
   person and the subject, is set a step down and in quieter tones than it was when
   it led the screen. The room above clears the site header's 2px keyline, or a
   notice when one sits above, and the gap between the two lines parts them as a
   heading and its caption (IW-631). */
.chat-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
  padding-top: var(--space-md);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--color-border-subtle);
}
.chat-header-line {
  align-self: stretch;
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  min-width: 0;
  font-size: var(--font-size-sm);
}
/* The title line is a button: pressing it opens About this boat. It reads as a
   heading at rest, and shows it can be pressed by its underline under the pointer
   and its ring under the keyboard, never by a filled control shape. A named boat
   on file's line is a link to its page instead, drawn the same (IW-910, below). */
.boat-title {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-sm);
  row-gap: 2px;
  max-width: 100%;
  min-width: 0;
  margin: 0;
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  font: inherit;
  text-align: left;
  color: var(--color-text-primary);
  cursor: pointer;
}
/* The hull, the name and the make are one group that never wraps: they give way
   inside it, the make first. The button around them wraps between this group and
   the place (IW-681), so a narrow pane moves the place under the name whole. */
.boat-title-lead {
  display: inline-flex;
  flex: 0 1 auto;
  align-items: baseline;
  gap: var(--space-sm);
  min-width: 0;
  max-width: 100%;
}
/* The hull mark: the line's one touch of the accent, because the line is the
   header's control. Centred on the name's cap height rather than on its baseline,
   since a glyph has none. */
.boat-title-hull {
  flex: none;
  align-self: center;
  width: 18px;
  height: 18px;
  color: var(--color-accent);
}
.boat-title-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-title);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
}
/* Boat Brand and Boat Model, in the secondary tone. They give way first in a tight
   pane, so the name keeps its letters longest. */
.boat-title-make {
  flex: 0 10 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-md);
  color: var(--color-text-secondary);
}
.boat-title-make:empty { display: none; }
/* Where the boat is now (IW-681), after the make: a pin and the place, in the
   make's size and tone so the name stays the only thing set as a title. It never
   squeezes the name: where the pane is too narrow for both it drops to a line of
   its own, and only there, alone on its line, does it shorten. Absent, not empty,
   when no location is on file. */
.boat-title-place {
  position: relative; /* the screen reader's "Location:" is positioned inside it */
  display: inline-flex;
  flex: 0 1 auto;
  align-items: baseline;
  gap: 3px;
  min-width: 0;
  max-width: 100%;
  font-size: var(--font-size-md);
  color: var(--color-text-secondary);
}
.boat-title-place[hidden] { display: none; }
.boat-title-place-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The country the place is in (IW-753), between the pin and the place: a small
   flag image served by the app, so it draws the same on Windows, where a flag
   emoji is two letters. Shorter than the line, so the line keeps its height
   whether one shows or not. The hairline keeps a white flag's edge on the light
   theme. */
.boat-title-flag {
  flex: none;
  align-self: center;
  width: 16px;
  height: 12px;
  margin-right: 2px;
  border-radius: 1px;
  box-shadow: 0 0 0 1px var(--color-border-subtle);
}
.boat-title-flag[hidden] { display: none; }
/* How old the location is, once it is 30 days old (IW-753): the smallest type
   in the muted tone, after the place, so it qualifies the place and never
   competes with it. It never wraps apart from the place it qualifies. */
.boat-title-age {
  flex: none;
  margin-left: 3px;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.boat-title-age[hidden] { display: none; }
/* The pin is drawn in the muted tone: the hull is the line's one accent. */
.boat-title-pin {
  flex: none;
  align-self: center;
  width: 13px;
  height: 13px;
  color: var(--color-text-muted);
}
.boat-title:hover .boat-title-name {
  text-decoration: underline;
  text-decoration-color: var(--color-accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.boat-title:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* ── The header's boat as a link to its page (IW-910) ─────────────────────────
   Pressing a named boat on file's line opens its page on Boats in a new tab. It is
   the same heading at rest, so it never takes a link's own underline: the name's
   accent underline under the pointer and the ring under the keyboard, above, are
   still the only signs it can be pressed, and the hover says where it goes. */
a.boat-title { text-decoration: none; }
/* No filled name yet: the line asks, quietly, and keeps its height so the screen
   does not move when a name arrives. The hull is drawn in outline dashes, a boat
   not yet known. */
.boat-title.is-unknown { color: var(--color-text-muted); }
.boat-title.is-unknown .boat-title-name { font-weight: 400; }
.boat-title.is-unknown .boat-title-hull {
  color: var(--color-text-muted);
  stroke-dasharray: 2 1.6;
}
.boat-title.is-unknown:hover { color: var(--color-text-secondary); }
/* The title line and the boat's No Foreign Land link after it (IW-754). The link
   is the button's sibling, so pressing it opens the boat's page and never About
   this boat; in a narrow pane it drops under the line whole rather than squeezing
   the name. */
.boat-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-md);
  row-gap: 2px;
  max-width: 100%;
  min-width: 0;
}
/* An outbound link in the Freshdesk link's own type, so the two read as the
   header's two ways out. The boat's own page is in the secondary tone; the search
   for its name, which is not the boat's page, stays muted. */
.boat-title-nfl {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  text-decoration: none;
  white-space: nowrap;
}
.boat-title-nfl.is-search { color: var(--color-text-muted); }
.boat-title-nfl[hidden] { display: none; }
.boat-title-nfl:hover { color: var(--color-accent); text-decoration: underline; }
.boat-title-nfl:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ── The header's unnamed boat (IW-823) ──────────────────────────────────────
   A ticket on a boat on file whose name was never filled in. The boat is known,
   so the hull stays solid, unlike "Which boat?"'s dashes. The missing name is
   what is quiet: "Unnamed boat" keeps the title's size, so the line does not
   jump when a name arrives, in a lighter weight and the secondary tone, so it
   never reads as the boat's name. The owner follows in the make's size and
   tone, and drops under the line whole in a narrow pane, like the place. Name
   it is the line's one call to act, so it takes the accent; a hairline frame
   tells it from the outbound links beside it. */
.boat-title.is-unnamed .boat-title-name {
  font-weight: 500;
  color: var(--color-text-secondary);
}
.boat-title-owner {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-md);
  color: var(--color-text-secondary);
}
.boat-title-owner[hidden] { display: none; }
.boat-title-name-it {
  flex: none;
  align-self: center;
  margin: 0;
  padding: 1px var(--space-sm);
  background: none;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 500;
  line-height: 1.4;
  color: var(--color-accent);
  white-space: nowrap;
  cursor: pointer;
}
.boat-title-name-it[hidden] { display: none; }
.boat-title-name-it:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-hover);
}
.boat-title-name-it:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* ── end of the header's unnamed boat (IW-823) ── */

/* ── The header's subject when there is no boat (IW-879) ──────────────────────
   Every ticket is about a boat, a partner company or a customer, and the first
   line names it in that order. With no boat, the partner or the customer takes
   the boat's place in the title's own type, so the header reads the same shape
   whoever it is about. Each kind carries its own mark where the boat has its
   hull: a yard building for a partner, a person for a customer. The hull is
   the accent because the boat line is the header's control; these lines open
   nothing, so their marks sit in the secondary tone and the line sets no
   pointer. On a customer's ticket the boat line shrinks to a small Which boat?
   prompt after the name, framed and in the accent like Name it, since it is
   the one step the line asks for. Before /ticket-meta answers, a line with no
   boat holds its height unseen, so Which boat? never flashes before a partner. */
.subject-title {
  display: inline-flex;
  flex: 0 1 auto;
  align-items: baseline;
  gap: var(--space-sm);
  min-width: 0;
  max-width: 100%;
  color: var(--color-text-primary);
}
.subject-title[hidden] { display: none; }
.subject-title-mark {
  flex: none;
  align-self: center;
  width: 18px;
  height: 18px;
  color: var(--color-text-secondary);
}
.subject-title-mark-partner,
.subject-title-mark-customer { display: none; }
.subject-title.is-partner .subject-title-mark-partner,
.subject-title.is-customer .subject-title-mark-customer { display: inline; }
.subject-title-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-title);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
}
.boat-title[hidden] { display: none; }
.boat-title.is-pending { visibility: hidden; }
.boat-title.is-prompt {
  flex: none;
  align-self: center;
  padding: 1px var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  color: var(--color-accent);
}
.boat-title.is-prompt .boat-title-hull,
.boat-title.is-prompt .boat-title-make,
.boat-title.is-prompt .boat-title-place,
.boat-title.is-prompt .boat-title-owner { display: none; }
.boat-title.is-prompt .boat-title-name {
  font-size: var(--font-size-sm);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0;
}
.boat-title.is-prompt:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-hover);
}
.boat-title.is-prompt:hover .boat-title-name { text-decoration: none; }
/* Who started a ticket a team member opened, after the person on the line under
   the title: a quiet note in the muted tone, a step smaller, so it never reads
   as the customer. The subject is what the line is for, so the note gives way
   first: the two share a box one line tall that wraps out of sight, laid out
   from the subject (row-reverse, the subject ordered first), so where both do
   not fit the note leaves whole and the subject keeps the share it had, giving
   way only to its own ellipsis, as the rail's second line does. */
.chat-header .ticket-about {
  display: flex;
  flex-direction: row-reverse;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: baseline;
  column-gap: var(--space-sm);
  flex: 0 1 auto;
  min-width: 0;
  height: calc(var(--font-size-sm) * 1.4);
  line-height: 1.4;
  overflow: hidden;
}
.chat-header .ticket-about > .ticket-subject { order: -1; flex: 0 1 auto; max-width: 100%; }
/* A rate limit notice wraps as a sentence, so the box grows to hold it. */
.chat-header .ticket-about:has(> .ticket-subject.is-notice) { height: auto; }
.chat-header .ticket-started {
  flex: none;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.chat-header .ticket-started:empty { display: none; }
/* Before /ticket-meta answers both are empty, and the box takes no gap. */
.chat-header .ticket-about:not(:has(> :not(:empty))) { display: none; }
/* ── end of the header's subject (IW-879) ── */
/* ── The header's RMA pills (IW-995) ─────────────────────────────────────────
   The ticket's RMA cards at the end of the title row, after NFL: a hairline
   framed pill holding the card key, five small stops showing how far the part
   has come, and the stage in plain words. The stops are the one bold thing; the
   rest is as quiet as the header's other parts. Colour comes only from the
   status tokens: grey blue while the part is on its way to shipping, green once
   shipped (the track alone) and delivered (filled), red when rejected, muted
   when cancelled. The accent stays the line's one step to take, so a pill takes
   it only as the keyboard ring. Never taller than the title's line, so the line
   keeps its height when a pill arrives; in a narrow pane the pills drop to a
   line of their own whole, through the row's own wrap. */
.rma-pills {
  display: inline-flex;
  flex: 0 1 auto;
  flex-wrap: wrap;
  align-items: center;
  align-self: center; /* centred on the line, so the line never grows by a pixel */
  gap: var(--space-xs) var(--space-sm);
  min-width: 0;
  max-width: 100%;
}
/* With no card to show the row holds nothing and takes no gap. */
.rma-pills:empty { display: none; }
.rma-pill {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
  padding: 1px var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  font-size: var(--font-size-sm);
  font-weight: 500;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
  text-decoration: none;
  white-space: nowrap;
}
a.rma-pill:hover .rma-pill-words { text-decoration: underline; text-underline-offset: 3px; }
.rma-pill:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.rma-pill-key { font-weight: 600; color: var(--color-text-primary); }
.rma-pill-words { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.rma-pill.is-delivered {
  border-color: transparent;
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
}
.rma-pill.is-rejected {
  border-color: transparent;
  background: var(--color-status-fault-bg);
  color: var(--color-status-fault);
}
.rma-pill.is-delivered .rma-pill-key,
.rma-pill.is-rejected .rma-pill-key { color: inherit; }
.rma-pill.is-cancelled,
.rma-pill.is-cancelled .rma-pill-key { color: var(--color-text-muted); }
/* The five stops: done ones filled, the current one a little larger with a soft
   ring, the ones to come an outline. A card off the path has none. */
.rma-stops {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 3px;
}
.rma-stop {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1.25px var(--color-text-muted);
  opacity: 0.55;
}
.rma-stop.is-done { background: var(--color-status-waiting); box-shadow: none; opacity: 1; }
.rma-stop.is-here {
  width: 8px;
  height: 8px;
  background: var(--color-status-waiting);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-status-waiting) 22%, transparent);
  opacity: 1;
}
/* Once the part has shipped the track turns green. */
.rma-pill.is-shipped .rma-stop.is-done,
.rma-pill.is-delivered .rma-stop.is-done { background: var(--color-status-resolved); }
.rma-pill.is-shipped .rma-stop.is-here,
.rma-pill.is-delivered .rma-stop.is-here {
  background: var(--color-status-resolved);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-status-resolved) 22%, transparent);
}
/* A third card and more fold into a quiet "+2 more", whose hover names them. */
.rma-more {
  position: relative; /* its screen reader names are positioned inside it */
  align-self: center;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}
/* ── end of the header's RMA pills (IW-995) ── */
.chat-header .ticket-id {
  font-size: inherit;
  font-weight: 600;
  color: var(--color-text-secondary);
  flex: none;
}
.chat-header .ticket-customer { color: var(--color-text-secondary); font-weight: 500; flex: none; }
.chat-header .ticket-subject {
  font-size: inherit;
  color: var(--color-text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A Freshdesk rate limit said in the subject's place (IW-604): a sentence, not a title,
   so it wraps rather than losing its wait to the ellipsis, a step smaller like the
   chat's other notices, and in their secondary tone. */
.chat-header .ticket-subject.is-notice {
  white-space: normal;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
/* Collapse the placeholders before /ticket-meta resolves so the gap stays tight. */
.chat-header .ticket-customer:empty,
.chat-header .ticket-subject:empty { display: none; }
.ticket-fd-link {
  margin-left: auto;
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  text-decoration: none;
  white-space: nowrap;
}
.ticket-fd-link:hover { color: var(--color-accent); text-decoration: underline; }

/* Ticket references (IW-286): a ticket number in the agent's text, the brief, or a
   decision card links to Freshdesk; the small button beside it opens the ticket in the
   workbench. The button is always drawn, quietly, as part of the number it belongs to:
   a hidden button (opacity 0) kept its width and left a two-space hole after every
   number in prose (IW-343, IW-459), and a button that appears on hover reflows the line
   under the pointer. The punctuation that closes a reference is drawn inside .ticket-ref,
   after the button, so the sentence reads "ticket 4054[open]?" with the mark against its
   own number and nothing the eye reads as a gap. The glyph sits below the cap height,
   like the underline, and lifts to the accent under the pointer or keyboard focus. */
.ticket-ref { white-space: nowrap; }
.ticket-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--color-text-muted);
  text-underline-offset: 2px;
}
.ticket-link:hover { color: var(--color-accent); text-decoration-color: currentColor; }
.ticket-open {
  display: inline-flex;
  align-items: center;
  vertical-align: -1px;
  /* 2px of air after the number's underline; the closing mark follows the glyph directly. */
  margin: 0;
  padding: 1px 0 1px 2px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  opacity: 0.7;
  cursor: pointer;
}
.ticket-open svg { width: 12px; height: 11px; display: block; }
.ticket-open:hover,
.ticket-open:focus-visible {
  color: var(--color-accent);
  background: var(--color-bg-hover);
  opacity: 1;
}

/* The transcript and its jump control (IW-290). The wrapper takes the flex growth the
   timeline used to take, and exists only so the control has the timeline's own bottom
   edge to sit on: positioned against .chat-workbench it would drift with the statusline
   and the command chips. */
.chat-timeline-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.chat-timeline {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* A wheel at the foot must not chain to the page: the transcript is the one scroller
     in this view, and scroll chaining is one of the ways the pin gets lost. */
  overscroll-behavior: contain;
  padding: var(--space-md) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* The room a send reserves beneath the operator's message, sized in chat.js. It is
   display:none at rest rather than zero-height, because the timeline is a flex column
   with a gap: a flat but displayed spacer would still take one gap of its own, both in
   the measurement chat.js makes and at the foot of every transcript. */
.chat-tail-spacer { flex: 0 0 auto; height: 0; }

.jump-latest {
  position: absolute;
  bottom: var(--space-md);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  box-shadow: var(--shadow-raised);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  white-space: nowrap; /* "Scroll down for the response" stays one line on a phone */
  cursor: pointer;
}
.jump-latest svg { width: 13px; height: 13px; display: block; }
.jump-latest:hover { color: var(--color-accent); border-color: var(--color-accent); }
.jump-latest[hidden] { display: none; }

.msg { max-width: 90%; }
.msg-user {
  align-self: flex-end;
  background: var(--color-bg-selected);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
}
/* ── The work log (IW-102) ────────────────────────────────────────────────────
 *
 * One assistant turn is one logged operation: passages of prose interrupted by quiet,
 * precise records of work. A hairline rail runs down the turn and ticks at every block
 * on it, so the shape of the turn — how much the agent said, how often it stopped to do
 * something — reads at a glance without opening anything.
 *
 * The rail is where this surface spends its boldness. The tick is the only place the
 * brand accent appears inside the transcript, and it carries a meaning rather than an
 * emphasis: a magenta tick is the agent acting, a hairline tick is the agent writing.
 * Everything else on the turn stays disciplined. */
.msg-assistant {
  align-self: flex-start;
  width: 100%;
  padding-left: var(--space-lg);
  border-left: 1px solid var(--color-ledger-rail);
}
.msg-text { overflow-wrap: anywhere; color: var(--color-text-primary); }

/* Ticks mark WORK, not every block. An earlier pass ticked the prose segments too, which
   made the tick a bullet rather than a signal: if every block has one, the rail stops
   telling you anything you could not already see. Unbroken rail beside prose, a tick
   where the agent did something, is the whole reading. */
.msg-assistant > .tool-group,
.msg-assistant > .artifact-card,
.msg-assistant > .approval-card,
.msg-assistant > .decision-card,
.msg-assistant > .proposal-card { position: relative; }
.msg-assistant > .tool-group::before,
.msg-assistant > .artifact-card::before,
.msg-assistant > .approval-card::before,
.msg-assistant > .decision-card::before,
.msg-assistant > .proposal-card::before {
  content: "";
  position: absolute;
  left: calc(var(--space-lg) * -1);
  width: var(--space-sm);
  height: 2px;
  background: var(--color-ledger-tick);
}
.msg-assistant > .tool-group::before { top: 0.85em; }
.msg-assistant > .artifact-card::before,
.msg-assistant > .approval-card::before,
.msg-assistant > .decision-card::before { top: calc(var(--space-md) + 0.6em); }
/* A proposal is the agent's block too (IW-362): it raised it, and the operator is being
   asked to act on it. Its padding is a step tighter than its siblings', so the tick takes
   that step to stay on the card's first line. Offsets are measured from inside the border,
   and this card's left edge is 3px where theirs is 1px, so it reaches 2px further to land
   on the same mark on the rail. */
.msg-assistant > .proposal-card::before {
  top: calc(var(--space-sm) + 0.6em);
  left: calc(var(--space-lg) * -1 - 2px);
}
/* A case note the assistant applied at once (IW-801). It is a ledger line, not a card:
   the agent acted, so it carries the rail's tick, and nothing asks the operator to
   decide, so it has no box and no filled button. The field names the change in the
   primary tone and the value follows in the secondary one, cut to a single line with
   the whole of it on hover. Undo is the one control, a quiet underlined word in the
   line's own tone, because the tick stays the only accent in the transcript. Undone,
   the line drops to muted and says who took it back. */
.case-note {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
  max-width: var(--measure-wide);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.msg-assistant > .case-note::before {
  content: "";
  position: absolute;
  top: 0.6em;
  left: calc(var(--space-lg) * -1);
  width: var(--space-sm);
  height: 2px;
  background: var(--color-ledger-tick);
}
.case-note-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.case-note-label { color: var(--color-text-primary); }
.case-note-undo {
  flex: none;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.case-note-undo:hover { color: var(--color-text-primary); }
.case-note-undo:disabled { color: var(--color-text-muted); cursor: default; }
.case-note-state { flex: none; color: var(--color-text-muted); }
.case-note-state:empty { display: none; }
.case-note-undone .case-note-text,
.case-note-undone .case-note-label { color: var(--color-text-muted); }
/* In a narrow pane the state takes a line of its own under the change, so the value
   a person took back, or the reason Undo no longer applies, is not cut to a word. */
@container pane (max-width: 560px) {
  .case-note { flex-wrap: wrap; row-gap: 0; }
  .case-note-text { flex: 1 1 0; }
  .case-note-state { flex-basis: 100%; }
}

/* An interrupted turn's rail goes dashed: the operation stopped partway rather than
   finishing, and the rail is the element that says so about the turn as a whole. */
.msg-assistant.msg-interrupted { border-left-style: dashed; }

/* Chronological segments: one turn holds a passage of prose per stretch of writing
   between tool calls, so the gap that used to sit only between turns now has to sit
   between the blocks inside one. */
.msg-assistant > * + * { margin-top: var(--space-md); }

/* /help output: a readable command list, one row per command, name aligned left. */
.help-block { display: flex; flex-direction: column; gap: var(--space-xs); }
.help-heading { font-weight: 600; margin-bottom: var(--space-xs, 6px); color: var(--color-text-primary); }
.help-row {
  display: grid;
  grid-template-columns: minmax(9rem, max-content) 1fr;
  gap: var(--space-sm, 10px);
  align-items: baseline;
}
.help-cmd {
  font-family: var(--font-mono, ui-monospace, monospace);
  color: var(--color-accent, var(--color-text-primary));
  white-space: nowrap;
}
.help-desc { color: var(--color-text-secondary, var(--color-text-primary)); }
/* User turns are escaped plain text, so preserve their line breaks. Assistant turns are
   rendered markdown (real block elements), so they manage their own spacing. */
.msg-user .msg-text { white-space: pre-wrap; }
/* A send the server refused or never received (IW-224): it is not in the archive. */
.msg-user-unsent { opacity: 0.6; }
.msg-unsent-note {
  margin-top: var(--space-xs);
  font-size: 0.85em;
  color: var(--color-text-muted);
}

/* Rendered markdown inside assistant turns. This is the one surface an operator reads
   for minutes at a time, so it takes a reading treatment rather than the chat-chrome
   scale: a step up in size, a looser leading, and a capped measure on running text.
   The measure is set on the paragraph-level children rather than on the block, so a
   wide table or a fenced code sample still uses the full pane. */
.msg-assistant .msg-text {
  font-size: var(--font-size-prose);
  line-height: var(--line-height-prose);
}
.msg-assistant .msg-text > p,
.msg-assistant .msg-text > ul,
.msg-assistant .msg-text > ol,
.msg-assistant .msg-text > blockquote,
.msg-assistant .msg-text > h1,
.msg-assistant .msg-text > h2,
.msg-assistant .msg-text > h3,
.msg-assistant .msg-text > h4 { max-width: var(--measure-prose); }
.msg-assistant .msg-text > pre,
.msg-assistant .msg-text > table { max-width: var(--measure-wide); }
.msg-assistant .msg-text > :first-child { margin-top: 0; }
.msg-assistant .msg-text > :last-child { margin-bottom: 0; }
.msg-assistant .msg-text p { margin: 0 0 var(--space-md); }
.msg-assistant .msg-text h1,
.msg-assistant .msg-text h2,
.msg-assistant .msg-text h3,
.msg-assistant .msg-text h4 {
  margin: var(--space-lg) 0 var(--space-sm);
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.msg-assistant .msg-text h1 { font-size: 1.4em; }
.msg-assistant .msg-text h2 { font-size: 1.25em; }
.msg-assistant .msg-text h3 { font-size: 1.1em; }
.msg-assistant .msg-text h4 { font-size: 1em; }
.msg-assistant .msg-text ul,
.msg-assistant .msg-text ol { margin: 0 0 var(--space-md); padding-left: 1.4em; }
.msg-assistant .msg-text li { margin: var(--space-xs) 0; }
.msg-assistant .msg-text li > p { margin: 0; }
.msg-assistant .msg-text li > ul,
.msg-assistant .msg-text li > ol { margin: var(--space-xs) 0 0; }
/* List markers recede: the item is the content, the bullet is punctuation. */
.msg-assistant .msg-text li::marker { color: var(--color-text-muted); }
.msg-assistant .msg-text a { color: var(--color-accent); text-decoration: underline; }
.msg-assistant .msg-text code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  background: var(--color-bg-code);
  padding: 1px 4px;
  border-radius: var(--radius-sm);
}
.msg-assistant .msg-text pre {
  margin: 0 0 var(--space-md);
  padding: var(--space-md);
  background: var(--color-bg-code);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  line-height: 1.5;
}
.msg-assistant .msg-text pre code { background: none; padding: 0; font-size: var(--font-size-sm); }
/* A quotation is someone else's voice: set in, lighter, and marked by a hairline rather
   than the heavy rule it used to carry, which competed with the ledger rail beside it. */
.msg-assistant .msg-text blockquote {
  margin: 0 0 var(--space-md);
  padding-left: var(--space-md);
  border-left: 1px solid var(--color-border-subtle);
  color: var(--color-text-secondary);
  font-style: italic;
}
/* Data tables read as instrument output: mono figures, tabular numerals so columns of
   readings line up, and horizontal rules only. Vertical borders on a small table make a
   grid the eye has to parse before it can read a value. */
.msg-assistant .msg-text table {
  border-collapse: collapse;
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
}
.msg-assistant .msg-text th,
.msg-assistant .msg-text td {
  border: none;
  border-bottom: 1px solid var(--color-border-subtle);
  padding: var(--space-xs) var(--space-md) var(--space-xs) 0;
  text-align: left;
}
.msg-assistant .msg-text th {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.msg-assistant .msg-text td { font-variant-numeric: tabular-nums; }
/* A word in a table never splits (IW-664). .msg-text above sets overflow-wrap: anywhere,
   which is right for prose in a narrow pane and wrong in a table: it lets the browser
   shrink a column below its longest word, so a setting name such as
   battery_integration_type came out as "battery_integration_ty" and "pe", and a name
   split that way gets mistyped. break-word still breaks a string that cannot fit on a
   line of its own, but it does not let the column shrink to make that happen, so every
   column stays at least as wide as its longest word.
   When the words alone are wider than the card, the table scrolls sideways inside its
   own box. display: block is what makes the table a scroll box; the rows still lay out
   as a table inside it. A block is never wider than what holds it, so the rule sets no
   max-width of its own and the measure caps set elsewhere (--measure-wide on a chat or
   brief table) still apply. The page and the card never scroll sideways. The judged
   evidence table is left out, since it sits in .evidence-scroll already. */
.msg-text table:not(.evidence-table) {
  display: block;
  overflow-x: auto;
}
.msg-text table:not(.evidence-table) th,
.msg-text table:not(.evidence-table) td {
  overflow-wrap: break-word;
  word-break: normal;
}
/* A short column (a default, a new value) holds one line, and the prose column takes what
   is left. chat.js tags the cells of both kinds (tagTableColumns), since only it can see
   that every cell in a column is short. A prose column keeps a readable line: without
   a floor, a table that has to scroll anyway squeezes its prose to one word a line and
   runs a screen tall.
   In a narrow pane the hold is dropped and the floor is lower, so a phone or a split
   screen wraps at word boundaries first and scrolls sideways only when the words
   themselves do not fit. A cell that has been given a control (the History row's
   Readings button) wraps too, so the control can drop under the text. */
.msg-text table .table-tight { white-space: nowrap; }
.msg-text table .table-tight:has(button) { white-space: normal; }
.msg-text table .table-prose { min-width: 14em; }
@container pane (max-width: 720px) {
  .msg-text table .table-tight { white-space: normal; }
}
@container pane (max-width: 560px) {
  .msg-text table .table-prose { min-width: 10em; }
}
/* A long link or code string with no space in it, tagged by chat.js, is the one thing
   allowed to break mid-string, so it does not push the whole table into scrolling. */
.msg-text table .table-long-token { overflow-wrap: anywhere; }
/* A date | who | ... table (the brief timeline, or anything shaped like it, tagged by
   chat.js from its own first two header cells rather than hardcoded here) reads at a
   glance only if the date and who columns hold their content on one line. The
   page-wide overflow-wrap: anywhere above is what breaks "2026-08-21" into four lines
   when a later column is long; nowrap on the date column (short and fixed-width,
   never needs to wrap) and a plain word-boundary wrap on the who column (so
   "Name (cust)" can break before the marker but never inside the name) undo that
   here, and the remaining column(s) absorb whatever width is left over (IW-284). */
.msg-assistant .msg-text table.date-first-table th:nth-child(1),
.msg-assistant .msg-text table.date-first-table td:nth-child(1) {
  white-space: nowrap;
}
.msg-assistant .msg-text table.date-first-table th:nth-child(2),
.msg-assistant .msg-text table.date-first-table td:nth-child(2) {
  overflow-wrap: normal;
  word-break: normal;
}
.msg-assistant .msg-text hr {
  border: none;
  border-top: 1px solid var(--color-border-subtle);
  margin: var(--space-md) 0;
}
.msg-error {
  margin-top: var(--space-xs);
  color: var(--color-status-fault);
  background: var(--color-status-fault-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--font-size-sm);
}
/* System notes (e.g. "Chat cleared"): a quiet, centered divider, not a chat bubble. */
.msg-system {
  align-self: center;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  text-align: center;
  padding: var(--space-xs) 0;
}
/* Rehydration divider (case-memory U4): a full-width rule marking where the
   conversation restarted on injected case memory; persisted, so it replays in place. */
.msg-system.msg-rehydrated {
  align-self: stretch;
  border-top: 1px dashed var(--color-border-subtle);
  margin-top: var(--space-sm);
  padding-top: var(--space-xs);
}
/* Earlier history on a long case (IW-372): the first paint draws the recent stretch, and
   this row at the top of the transcript brings the rest back a stretch at a time. It is
   navigation, not conversation, so it sits quiet and centred like a system note, with the
   outline button's weight rather than a filled one. */
.chat-older {
  align-self: center;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  padding: var(--space-xs) 0;
}

/* The loading notice (IW-538): what the top of the chat says while a ticket opens, so the
   pause before the first content never reads as hung. It is a status about the whole
   pane, not a turn in the conversation, so it sits centred like a system note and a step
   below the header rather than flush under it. The title is the one line that matters
   and takes the chrome's own size; everything under it is a step smaller and quieter.

   The bar is the statusline's progress bar (IW-395), and is drawn by the statusline's
   own rules further down, which name both: one track, one fill, one travel, one still
   state and one reduced-motion treatment, so the two read as one instrument and cannot
   drift apart. The operator who has watched a turn run already knows what this one
   means. Only what differs is here. States, set as classes by chat.js:
     is-loading  the segment travels; the title alone is shown
     is-slow     the open is far past its usual time: the track is lit and still, the
                 title steps up to the primary tone, and the words say what to do
     is-waiting  the catch-up check is waiting out a Freshdesk rate limit (IW-604): the
                 track is lit and still, as the statusline's waiting state is, and the
                 title alone says how long. Nothing is wrong, so no tone steps up and no
                 reload is offered: a reload inside the wait spends the allowance again
     is-failed   the open was refused or failed: no bar at all, since nothing is under
                 way, and the title takes the fault tone as text only. A filled fault
                 block is the error row's, and this is a notice that may yet clear. */
.chat-loading {
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  max-width: min(100%, 46ch);
  margin-top: var(--space-xl);
  padding: 0 var(--space-md);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
/* display is set on the box's children below, and a bare display beats [hidden]. */
.chat-loading > [hidden] { display: none; }
.chat-loading-title {
  color: var(--color-text-secondary);
  font-size: var(--font-size-md);
}
.chat-loading.is-slow .chat-loading-title { color: var(--color-text-primary); }
.chat-loading.is-failed .chat-loading-title { color: var(--color-status-fault); }
.chat-loading.is-failed .chat-loading-bar { display: none; }
/* The reason is the server's sentence and reads as the body; the advice is ours and
   belongs to it, so it sits half a gap closer than the box's other rows. */
.chat-loading-reason { color: var(--color-text-secondary); }
.chat-loading-advice { margin-top: calc(-1 * var(--space-xs)); }
/* A button takes the system face unless told otherwise; this one speaks in the notice's. */
.chat-loading-reload { margin-top: var(--space-xs); font-family: inherit; }
/* The catch-up notice (IW-558): the same notice at the foot of a reopened conversation,
   under the restart divider, while the check for newer messages runs. It sits in the
   conversation's flow and answers the row above it, so it takes the timeline's own gap
   and not the open notice's step down from the header, which would set it adrift. */
.chat-loading.chat-loading-foot { margin-top: 0; }

/* Backstage tool activity: one quiet, recessive group per turn. The group head is a muted
   summary line; the individual tool entries sit indented behind a thin rail, collapsed by
   default so the brief and the recommendation carry the visual weight. */
/* The group head reads as a LOG LINE, not a widget. Mono is what separates work from
   prose here; the two faces carry the distinction so colour barely has to participate.
   The caret appears on hover, focus, or while expanded, so a settled turn is a clean
   column of records rather than a row of controls waiting to be clicked. */
.tool-group { margin-top: var(--space-xs); }
.tool-group-head {
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  padding: 2px 0;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.01em;
  color: var(--color-text-muted);
}
.tool-group-head:hover { color: var(--color-text-secondary); }
.tg-caret {
  color: var(--color-text-muted);
  opacity: 0;
}
.tool-group-head:hover .tg-caret,
.tool-group-head:focus-visible .tg-caret,
.tool-group-head[aria-expanded="true"] .tg-caret { opacity: 1; }
.tool-group-body {
  margin: var(--space-xs) 0 0 var(--space-xs);
  padding-left: var(--space-md);
  border-left: 1px solid var(--color-ledger-rail);
  flex-direction: column;
  gap: var(--space-xs);
}
/* display only when shown: a bare `display: flex` would beat the [hidden] attribute's
   display:none, so the collapsed group would never actually hide its entries. */
.tool-group-body:not([hidden]) { display: flex; }
/* Entries are the ledger's line items: tool names are identifiers and elapsed times are
   figures, so both take the mono face at the ledger's size. */
.tool-entry { font-size: var(--font-size-xs); font-family: var(--font-mono); }
.tool-head {
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  padding: 2px 0;
  font-family: inherit;
  font-size: inherit;
  color: var(--color-text-secondary);
}
.tool-name { color: var(--color-text-primary); }
.tool-elapsed { color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.tool-caret { color: var(--color-text-muted); }
.tool-running .tool-name::after { content: " …"; color: var(--color-text-muted); }
.tool-entry.tool-error .tool-name { color: var(--color-status-fault); }
.tool-body, .tool-stderr {
  margin: 0;
  padding: var(--space-sm);
  background: var(--color-bg-code);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  overflow-x: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.tool-stderr { color: var(--color-status-fault); }
/* The plain lead beside a tool name, and the raw input one disclosure inside the body
   (IW-270): opening an entry shows what the call does before any JSON. */
.tool-lead { color: var(--color-text-muted); }
.tool-body .change-body { font-family: var(--font-sans); white-space: normal; }
.tool-raw > summary { cursor: pointer; color: var(--color-text-secondary); }
.tool-raw-input { margin: var(--space-xs) 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* An interrupted turn (IW-102 U3). The session died before the turn's terminal event, so
   the end-of-replay sweep settles it here; without this the entries spin and the summary
   reads "Working…" on every reopen, forever. The treatment is a record, not an alarm:
   muted throughout, no fault colour, because a stream that died upstream is a fact about
   the session and not a failure the operator caused or can act on. */
.tool-stalled .tool-name::after { content: " (stopped)"; color: var(--color-text-muted); }
.tool-group-interrupted .tool-group-head { color: var(--color-text-muted); }
.msg-interrupted-note {
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

/* ── Motion (IW-102 U6) ───────────────────────────────────────────────────────
 *
 * One orchestrated gesture, not a collection of effects: a block arrives with a short
 * fade and a small rise, and the group head cross-fades when its state changes.
 * Streaming text itself never animates; the text arriving IS the animation.
 *
 * The entrance class is applied by chat.js only on LIVE events. Replay applies none, so
 * a reopened case paints at once rather than cascading dozens of elements, and the
 * scroll-to-end pinning stays exact (animated late height changes would unpin it). */
@media (prefers-reduced-motion: no-preference) {
  .wb-enter { animation: wb-rise var(--motion-enter) var(--ease-out) both; }
  .tool-group-head,
  .tg-caret { transition: color var(--motion-enter) var(--ease-out),
                          opacity var(--motion-enter) var(--ease-out); }
}
@keyframes wb-rise {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* Approval cards */
.approval-card {
  margin-top: var(--space-sm);
  border: 1px solid var(--color-status-warning);
  background: var(--color-status-warning-bg);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  max-width: var(--measure-wide);
}
/* The pending card is the one place a title must carry weight: it is a question, and the
   operator has to see they are being asked something. A settled card drops back to the
   quiet label above, because a decided gate is a record of what happened. */
.approval-card.approval-pending .approval-title {
  font-size: var(--font-size-md);
  font-weight: 700;
  color: var(--color-text-primary);
}
.approval-desc { color: var(--color-text-secondary); margin: var(--space-xs) 0; }
.approval-input {
  margin: var(--space-xs) 0;
  padding: var(--space-sm);
  background: var(--color-bg-code);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  overflow-x: auto;
}
.approval-actions { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-sm); }
.approval-status { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.approval-historical { opacity: 0.65; border-style: dashed; }
.approval-note { color: var(--color-text-muted); font-size: var(--font-size-sm); margin-top: var(--space-xs); }
/* The plain-language body (IW-270): the path as a muted aside under the lead, the agent's
   reason, and the change itself as readable text. The exact tool input sits behind the
   disclosure below on every card, because the readable body is the visible change. */
.approval-path {
  margin: 0 0 var(--space-xs);
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  overflow-wrap: anywhere;
}
.approval-reason { color: var(--color-text-primary); margin: var(--space-xs) 0; }
.change-body { margin: var(--space-xs) 0; }
.change-block { margin: var(--space-xs) 0; }
.change-label {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-weight: 600;
}
.change-text {
  margin: 2px 0 0;
  padding: var(--space-sm);
  background: var(--color-bg-code);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 16rem;
  overflow: auto;
}
.change-note { color: var(--color-text-secondary); font-size: var(--font-size-sm); margin: var(--space-xs) 0; }
.change-fields { margin: var(--space-xs) 0; }
.change-field-name { color: var(--color-text-secondary); font-size: var(--font-size-sm); font-weight: 600; }
.change-field-value { margin: 0 0 var(--space-xs); overflow-wrap: anywhere; }
.approval-details { margin: var(--space-xs) 0; }
.approval-details > summary {
  cursor: pointer;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}
/* A settled card is a record, not a request: the amber warning ground goes away with
   the controls, and the border alone carries which outcome it holds. IW-102's U5
   owns the finished treatment of all four states; this is the minimum that stops a
   resolved card reading as open. */
.approval-settled { background: var(--color-bg-surface); }
.approval-decision {
  margin-top: var(--space-sm);
  font-weight: 600;
  font-size: var(--font-size-sm);
}
/* The settled outcome takes focus when the operator's own click removed the button
   they were on, so it needs the shared ring the global rule only gives to genuinely
   interactive elements. */
.approval-decision:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.approval-allowed { border-color: var(--color-status-resolved); }
.approval-allowed .approval-decision { color: var(--color-status-resolved); }
.approval-denied { border-color: var(--color-status-fault); }
.approval-denied .approval-decision { color: var(--color-status-fault); }
/* Gate nobody decided: its turn died or its session was torn down. Muted and inert,
   deliberately NOT the fault tone, because no one refused anything. */
.approval-stale { opacity: 0.6; border-style: dashed; border-color: var(--color-status-waiting); }
.approval-stale .approval-decision { color: var(--color-text-muted); font-weight: 400; }

/* Decision cards (IW-269): a question the agent asked, answered by a click that becomes
   the operator's next turn. Same frame as an approval card, in the accent tone rather
   than the warning tone, because nothing is being written; settled, it is a record. */
.decision-card {
  margin-top: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  background: var(--color-bg-raised);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  max-width: var(--measure-wide);
}
.decision-card.decision-pending { border-color: var(--color-accent); }
.decision-question { font-size: var(--font-size-md); color: var(--color-text-primary); }
.decision-card.decision-pending .decision-question { font-weight: 700; }
.decision-cost { color: var(--color-text-secondary); font-size: var(--font-size-sm); margin-top: var(--space-xs); }
/* The buttons wrap rather than overflow: a menu card (IW-457) holds seven, and even
   three do not fit a phone width on one line. */
.decision-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-top: var(--space-sm); }
/* A menu (five or more options, IW-457) has no expected answer, so no button leads.
   The answers sit in an even grid, one column on a phone and up to three across the
   card, so six models read as a pick list rather than a ragged wrap. The last option
   is the way out: its own row under the same rule a proposal's effect line uses, so
   it reads as a different kind of answer and not as a seventh model. */
.decision-menu .decision-actions { display: block; }
.decision-menu-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: var(--space-sm);
}
.decision-menu-grid .btn-secondary { text-align: left; }
.decision-way-out {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.decision-status { color: var(--color-text-muted); font-size: var(--font-size-sm); }
/* The agent's turn is parked on this card (IW-310). A heavier edge than the ordinary
   pending state, because nothing else in the conversation moves until it is answered. */
.decision-card.decision-parked { border-width: 2px; }
.decision-settled { background: var(--color-bg-surface); }
.decision-choice { margin-top: var(--space-sm); font-weight: 600; font-size: var(--font-size-sm); }
.decision-choice:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.decision-chosen .decision-choice { color: var(--color-text-primary); }
.decision-answered-elsewhere .decision-choice { color: var(--color-text-muted); font-weight: 400; }

/* The boat's earlier RMA and SSQ cards (IW-599). It leads the decision card that starts
   a filing, above the question, so the operator reads what is already on file before
   choosing Create new. It is a ledger, so it is set the way the brief Timeline sets its
   card rows: the key in the mono face, linked to Jira, then the facts in plain text,
   a step down for what is detail.
   The one strong mark is the stripe on an OPEN card, in the warning tone: an open card
   on this boat is the reason to stop before filing a second one. A closed card keeps the
   border tone, and the Open and Closed labels say the same thing in words, so the
   colour is never the only copy. The kind is an outlined chip in the muted tone, like a
   registry family chip, so it reads as a classification and never as a status. The
   history closes under a rule, and the question and buttons sit below it unchanged. */
.ch {
  margin-bottom: var(--space-md);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--color-border-subtle);
}
.ch-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-sm); }
.ch-boat { font-size: var(--font-size-md); font-weight: 700; color: var(--color-text-primary); }
.ch-boat-key {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.ch-summary { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin-top: var(--space-xs); }
.ch-unread { color: var(--color-status-warning); }
.ch-group { margin-top: var(--space-md); }
.ch-group-label { font-size: var(--font-size-xs); font-weight: 600; color: var(--color-text-secondary); }
.ch-list { list-style: none; margin: 0; padding: 0; }
.ch-row {
  margin-top: var(--space-sm);
  padding: var(--space-xs) 0 var(--space-xs) var(--space-md);
  border-left: 3px solid var(--color-border-subtle);
  min-width: 0;
}
.ch-row.ch-open { border-left-color: var(--color-status-warning); }
.ch-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-xs) var(--space-sm); }
.ch-key {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
a.ch-key { color: var(--color-accent); text-decoration: none; }
a.ch-key:hover { text-decoration: underline; text-underline-offset: 2px; }
a.ch-key:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.ch-type {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-xs);
  line-height: 1.5;
}
.ch-status { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.ch-open .ch-status { color: var(--color-status-warning); font-weight: 600; }
.ch-parts {
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
  margin-top: 2px;
  overflow-wrap: anywhere;
}
.ch-meta,
.ch-moves {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin-top: 2px;
  overflow-wrap: anywhere;
}
.ch-more { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin-top: var(--space-sm); }
/* Search older's own section: cards that only name the requester or the vessel name.
   Under the same rule a decision's way out sits under, so it reads as a different kind
   of answer and never as more of this boat's history. */
.ch-soft {
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.ch-soft-head { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-primary); }
/* The Possibly related card /boat-history ends with (IW-612). It is the same ledger as
   the boat's earlier cards, so its rows are .ch rows, and it says on its face that none
   of it is the boat's record: every row is in the closed (border) tone, never the open
   stripe, because nothing here has happened to this boat as far as the record knows. The
   row action, Belongs to this boat, is the outline small button, a step below the
   decision buttons; its outcome sits beside it in the muted tone. A row the operator
   linked (IW-761) is the boat's record now, so its stripe and its line take the resolved
   tone, and Undo beside the line is a text control in the accent, the register of Add to
   the Boat timeline, so it never competes with the line it qualifies. These all belong
   leads the list in the row button's own outline. Show the updated history closes the
   list once anything is linked: the one next step, so it alone wears the accent outline.
   Not searched closes the card under the same rule Search older's section sits under. */
.boat-related-card .ch-list { margin-top: var(--space-xs); }
.br-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-xs);
}
.br-actions[hidden] { display: none; }
.br-all { margin-top: var(--space-sm); }
.br-history { margin-top: var(--space-md); }
.ch-key.br-page { font-family: var(--font-sans); white-space: normal; }
.br-state { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.br-state:empty { display: none; }
.ch-row.br-linked { border-left-color: var(--color-status-resolved); }
.br-linked .br-state { color: var(--color-status-resolved); font-weight: 600; }
.br-undo {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-accent);
  cursor: pointer;
}
.br-undo[hidden] { display: none; }
.br-undo:hover,
.br-undo:focus-visible {
  color: var(--color-accent-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.br-undo:disabled { cursor: default; opacity: 0.6; text-decoration: none; }
/* Not this boat (IW-776): the second answer on a ticket row, beside Belongs to this
   boat. A marked row keeps the neutral rule and sets its ticket in the muted ink, so
   it reads as set aside, while its Undo stays at full strength. */
.ch-row.br-marked .ch-key,
.ch-row.br-marked .ch-parts { color: var(--color-text-muted); }
/* Rebuild the brief with these tickets (IW-763): the history's last word, under the
   history button, in its accent outline, since it is the other next step the card
   offers. Its sentence sits in the card's summary register above it. */
.br-fold { margin-top: var(--space-md); }
.br-fold[hidden] { display: none; }
.br-fold .br-fold-text { margin: 0; }
.btn-small.br-history-button { border-color: var(--color-accent); color: var(--color-accent); font-weight: 500; }
.btn-small.br-history-button:disabled { border-color: var(--color-border-subtle); color: var(--color-text-muted); }

/* Artifact and approval cards sit on the same rail as everything else in the turn, so
   they share one padding, one radius, and one heading treatment (IW-102 U5). The card
   title is an eyebrow, not a headline: the card's CONTENT is what the operator reads,
   and a bold heading above it competes with the prose it interrupted. Card internals
   (brief sections, evidence tables, draft controls) are untouched. */
.artifact-card {
  margin-top: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  padding: var(--space-md);
  max-width: var(--measure-wide);
}
.artifact-title,
.approval-title {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-sm);
}
.brief-body { margin-top: var(--space-xs); }
/* Brief card rhythm (IW-283): the blocks inside a section sit one --space-md apart, so
   the card reads as blocks rather than one run of paragraphs. Since IW-437 the body's
   children are the head, the sections and the context group, so the rhythm is set on
   the section's children in the reading-order block below; the server-rendered blocks
   take the same gap here. */
.brief-card .brief-boat,
.brief-card .evidence-block { margin-top: var(--space-md); }
/* The Boat block's Details disclosure (IW-288): every registry field, kept on the card
   but behind the plain sentences, so it reads as secondary like the Evidence fold. */
.brief-card .brief-boat-details { margin-top: var(--space-sm); }
.brief-card .brief-boat-details > summary { cursor: pointer; color: var(--color-text-secondary); }
/* The hypothesis callout (IW-283, lifted IW-332). The one claim the card is built
   around, so the eye lands on it before anything else on the screen. Since IW-472 it
   sits under the customer's current issue and above the record it rests on, and the
   lift is what makes it the first thing seen. It stays
   inline in the brief body rather than becoming a surface of its own, because the card
   has to keep reading as one chart: an accent tint, a thin accent edge, room around it,
   and a raised shadow lift it off the card without detaching it. Never a warning or
   fault tone, because a hypothesis is not a finding. Upright and in the primary text
   colour, undoing the quotation treatment the skill's blockquote would otherwise
   inherit. */
.brief-card .brief-body .brief-hypothesis {
  max-width: var(--measure-prose);
  padding: var(--space-lg) var(--space-xl);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: var(--color-bg-page);
  background: color-mix(in srgb, var(--color-accent) 8%, transparent);
  box-shadow: var(--shadow-raised);
  color: var(--color-text-primary);
  font-style: normal;
  line-height: 1.6;
}
/* One element per quoted line, split out of the rendered paragraph by chat.js so the
   claim and the grade can be sized and toned individually (IW-332). */
.brief-card .brief-body .brief-hypothesis p,
.brief-card .brief-body .brief-hypothesis .hyp-line { margin: 0 0 var(--space-xs); }
.brief-card .brief-body .brief-hypothesis ol,
.brief-card .brief-body .brief-hypothesis ul { margin: 0; padding-left: 1.6em; }
.brief-card .brief-body .brief-hypothesis > :last-child { margin-bottom: 0; }
/* The label line carries the grade, at label size: the claim holds the visual weight,
   because an oversized number reads as a measurement and this number is a judgement. */
.brief-card .brief-body .brief-hypothesis .hyp-label {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-sm);
}
.brief-card .brief-body .brief-hypothesis .hyp-claim {
  font-size: var(--font-size-claim);
  line-height: 1.4;
  margin-bottom: var(--space-md);
}
/* The grade's three bands (IW-332). Colour is the carrier of the band, with the number
   itself as the fallback for a reader who cannot separate the hues. The three roles are
   the grade's own, tuned off the fault and resolved tones, so an unproven claim never
   wears either whatever its grade. */
.brief-card .brief-body .brief-hypothesis .hyp-grade { font-weight: 600; }
.brief-card .brief-body .brief-hypothesis .hyp-grade-low { color: var(--color-confidence-low); }
.brief-card .brief-body .brief-hypothesis .hyp-grade-middle { color: var(--color-confidence-middle); }
.brief-card .brief-body .brief-hypothesis .hyp-grade-high { color: var(--color-confidence-high); }
/* The closest earlier match on the same boat (IW-763), at the head of the basis.
   It is a fact from the record, not the claim, so it never takes the claim's size; it is
   the most valuable fact the card can carry, so it is the one line in the callout set
   off on its own: a resolved-tone edge and lead, the tone of a ticket that was fixed,
   since that is what it reports. The text stays primary, read like any other line. */
.brief-card .brief-body .brief-hypothesis .hyp-earlier-fix,
.brief-card .brief-body .brief-earlier-fix-alone {
  padding-left: var(--space-sm);
  border-left: 2px solid var(--color-status-resolved);
  margin-bottom: var(--space-sm);
}
.brief-card .brief-body .brief-earlier-fix-alone { max-width: var(--measure-prose); }
.brief-card .brief-body .hyp-earlier-fix > strong:first-child { color: var(--color-status-resolved); }
/* No hypothesis at all (IW-332). The block is still there, still lifted, so the operator
   reads what to collect next instead of finding it missing from the card. Quieter than
   the asserting state: the accent tint and edge drop away, because there is no claim to
   put weight behind. Declared after the asserting rule at equal specificity, so it wins
   on order. */
.brief-card .brief-body .brief-hypothesis-none {
  border-left-color: var(--color-border-subtle);
  background: var(--color-bg-page);
  color: var(--color-text-secondary);
}
/* The support strategies the next move follows (IW-679). They close the Next
   move section as the source of its order: which of the team's rules shaped the
   list above, and on which ticket it was taught. So they take the mark the
   product draws beside somebody's own words, the quote's hairline rule, and
   the small secondary voice of a source line. No fill and no colour of their
   own: the hypothesis and what the operator asked are the card's two callouts,
   and a citation must not read as a third. The name carries the weight, since
   it is what an operator who knows the strategy recognises; the rule under it
   is for the one who does not. */
.brief-card .brief-body .brief-strategies {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-width: var(--measure-prose);
  margin: 0;
}
.brief-card .brief-body .brief-strategy {
  padding-left: var(--space-sm);
  border-left: 2px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.brief-card .brief-body .brief-strategy-cite,
.brief-card .brief-body .brief-strategy-rule {
  max-width: none;
  margin: 0;
  overflow-wrap: anywhere;
}
.brief-card .brief-body .brief-strategy-cite {
  font-weight: 600;
  color: var(--color-text-secondary);
}
.brief-card .brief-body .brief-strategy-rule { color: var(--color-text-muted); }

/* The playbooks the brief followed (IW-678). They close the open sections as the
   source of the procedure the brief ran, so they are drawn exactly as the
   strategies under Next move are: the hairline rule the product puts beside
   somebody's own words, the citation carrying the weight, the trigger under it
   a step quieter. One family of source lines on the card, not two. */
.brief-card .brief-body .brief-playbooks {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-width: var(--measure-prose);
  margin: 0;
}
.brief-card .brief-body .brief-playbook {
  padding-left: var(--space-sm);
  border-left: 2px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.brief-card .brief-body .brief-playbook-cite,
.brief-card .brief-body .brief-playbook-trigger {
  max-width: none;
  margin: 0;
  overflow-wrap: anywhere;
}
.brief-card .brief-body .brief-playbook-cite {
  font-weight: 600;
  color: var(--color-text-secondary);
}
.brief-card .brief-body .brief-playbook-trigger { color: var(--color-text-muted); }

/* What the operator asked for (IW-665). Words typed after the brief command are
   answered once, in this block, directly under the hypothesis callout and above the
   next move. It is built like the callout so the two read as one family: a tint, a 3px
   left edge, the same radius and the same raised shadow, and the same side padding so
   their text shares a left edge. It is cobalt, never the accent, so the two are not
   confused, and it is second to the callout by three deliberate steps: a weaker tint
   (6% against 8%), tighter padding above and below, and no line at the claim size, the
   answer staying at the body size. The label line is the reason the block is there:
   "You asked" in the block's own colour, then the operator's question as they typed
   it, at label size. Links keep the normal link colour. Prose keeps the prose measure;
   a block holding a table takes the wider one, and the table inside it scrolls on its
   own if a phone cannot fit it.
   The same block stands on its own in the chat (IW-666) when boat history or the latest
   reply was steered: .asked-card is only somewhere for it to stand, with the gap a card
   takes from the text above it. Every rule below names both places, so there is one
   rule set and the two cannot drift. */
.asked-card .brief-asked,
.brief-card .brief-body .brief-asked {
  max-width: var(--measure-prose);
  padding: var(--space-md) var(--space-xl);
  border-left: 3px solid var(--color-asked);
  border-radius: var(--radius-md);
  background: var(--color-bg-page);
  background: color-mix(in srgb, var(--color-asked) 6%, transparent);
  box-shadow: var(--shadow-raised);
  color: var(--color-text-primary);
  line-height: 1.6;
}
.asked-card .brief-asked-wide,
.brief-card .brief-body .brief-asked-wide { max-width: var(--measure-wide); }
.asked-card .brief-asked .asked-label,
.brief-card .brief-body .brief-asked .asked-label {
  margin: 0 0 var(--space-sm);
  max-width: none;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.asked-card .brief-asked .asked-lead,
.brief-card .brief-body .brief-asked .asked-lead {
  margin-right: var(--space-xs);
  font-weight: 600;
  color: var(--color-asked);
}
.asked-card .brief-asked .asked-answer > p,
.asked-card .brief-asked .asked-answer > ul,
.asked-card .brief-asked .asked-answer > ol,
.asked-card .brief-asked .asked-answer > blockquote,
.brief-card .brief-body .brief-asked .asked-answer > p,
.brief-card .brief-body .brief-asked .asked-answer > ul,
.brief-card .brief-body .brief-asked .asked-answer > ol,
.brief-card .brief-body .brief-asked .asked-answer > blockquote {
  max-width: var(--measure-prose);
  margin: 0 0 var(--space-sm);
}
.asked-card .brief-asked .asked-answer > ul,
.asked-card .brief-asked .asked-answer > ol,
.brief-card .brief-body .brief-asked .asked-answer > ul,
.brief-card .brief-body .brief-asked .asked-answer > ol { padding-left: 1.6em; }
.asked-card .brief-asked .asked-answer > table,
.brief-card .brief-body .brief-asked .asked-answer > table {
  max-width: 100%;
  margin: 0 0 var(--space-sm);
}
.asked-card .brief-asked .asked-answer > :last-child,
.brief-card .brief-body .brief-asked .asked-answer > :last-child { margin-bottom: 0; }
.asked-card { margin-top: var(--space-md); }
/* The draft box fits its text (IW-523, replacing the 60vh floor IW-189 set on every
   draft). chat.js writes the textarea's height from its content; the html surface is
   a div and fits on its own. Both rest on a floor of three line boxes plus the padding
   and the borders, so an empty or one-line draft still reads as editable, and a
   ceiling of 60% of the window, past which the text scrolls inside the box and the
   buttons stay directly below it. The line height is stated on the textarea because
   the floor is built from it, the same sum the composer's floor uses. */
.draft-edit {
  width: 100%;
  min-height: calc(3 * 1.5 * var(--font-size-md) + 2 * var(--space-sm) + 2px);
  max-height: 60vh;
  overflow-y: auto;
  resize: vertical;
  margin-top: var(--space-xs);
  font-family: var(--font-sans);
  font-size: var(--font-size-md);
  line-height: 1.5;
  padding: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
}
.draft-actions { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-xs); }
.draft-status { color: var(--color-text-muted); font-size: var(--font-size-sm); }
/* The Post to Freshdesk outcome on a note card (IW-555), said in the status line in the
   status colour it is: posted, refused, or sent with no answer back. */
.draft-status.draft-status-posted { color: var(--color-status-resolved); }
.draft-status.draft-status-failed { color: var(--color-status-fault); }
.draft-status.draft-status-unknown { color: var(--color-status-warning); }
/* A teammate's Integra note on this ticket stays quiet: context, not an outcome. */
.draft-status.draft-status-teammate { color: var(--color-text-secondary); }
/* A posted note is locked: the box gives up its edit border, as a filed Docs update
   card's fields do, so it reads as the record of what went out. */
.draft-posted .draft-edit,
.draft-posted .draft-html { border-color: transparent; background: var(--color-bg-page); }
/* A locked draft (IW-656): its reply or note went out, or a newer draft of its kind
   replaced it. It keeps its full height and every word, so the conversation still
   reads as it happened, and gives up what made it look live: the edit border and
   surface go, as on a posted note, and the text drops to the muted tone. The
   box cannot be resized, because there is nothing left to do in it. The stamp in
   the title row says which it is, in the badge shape every other state on the
   screen uses: the resolved tint for sent or posted, grey for replaced. */
.draft-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-sm); }
.draft-locked .draft-edit,
.draft-locked .draft-html {
  border-color: transparent;
  background: var(--color-bg-page);
  color: var(--color-text-muted);
  resize: none;
}
/* The stamp is drawn with every card and shown only once there is something to say;
   the badge's own display would otherwise show it as an empty pill. */
.draft-stamp[hidden] { display: none; }
.draft-locked .draft-html img { opacity: 0.6; }
.draft-locked .draft-html a { color: inherit; }
/* Whose words a sent card shows (IW-674). The caption sits after the stamp and is
   plain text, not a second badge: the stamp is the card's state, and this only says
   which version of the text is in the box, "As sent" or "As drafted". When the sent
   words differ from the draft, the draft is kept in a fold under the box. Its summary
   is a quiet line of words in sentence case, set against the shared section register
   (capitals, a filled strip) on purpose, since it is a way back to an earlier text
   and not a section of the card. The draft itself is set like the locked text above
   it, with a rule down its left edge in place of a box, the mark this screen already
   uses for a quoted earlier message. */
.draft-version { font-weight: 400; color: var(--color-text-muted); }
.draft-html.draft-html-plain { white-space: pre-wrap; }
.draft-original { margin: var(--space-xs) 0 0; border: none; border-radius: 0; }
.draft-original > summary {
  display: inline-block;
  padding: 0;
  font-size: var(--font-size-sm);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  background: none;
}
.draft-card .draft-original-body {
  min-height: 0;
  max-height: 60vh;
  overflow-y: auto;
  margin: var(--space-xs) 0 var(--space-sm);
  padding: 0 var(--space-sm);
  border: 0;
  border-left: 2px solid var(--color-border-subtle);
  border-radius: 0;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-md);
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.draft-original-text { white-space: pre-wrap; }

/* Rich (html) draft: rendered formatting + inline images, edited in place (no raw HTML). */
.draft-html {
  width: 100%;
  min-height: calc(3 * 1.5 * var(--font-size-md) + 2 * var(--space-sm) + 2px);
  max-height: 60vh;
  overflow-y: auto;
  margin-top: var(--space-xs);
  padding: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  font-size: var(--font-size-md);
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.draft-html:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }
.draft-html img { max-width: 100%; height: auto; display: block; margin: var(--space-sm) 0; border-radius: var(--radius-sm); }
.draft-html p { margin: 0 0 var(--space-sm); }
.draft-html ul, .draft-html ol { margin: 0 0 var(--space-sm); padding-left: 1.4em; }
.draft-html > :first-child { margin-top: 0; }
.draft-html > :last-child { margin-bottom: 0; }
.brief-evidence { margin-top: var(--space-sm); }
.brief-evidence summary { cursor: pointer; color: var(--color-text-secondary); }

/* The card's reading order (IW-437, reordered IW-472). chat.js rebuilds the rendered
   body into sections in the order the operator works: a matched hardware warning,
   what the customer needs now (the current issue), what we think (the callout), what
   to do (the next move), then the record it rests on (the timeline, the latest
   capture, the observations, the gaps), then the context in one closed group. The
   composition is what changes here; the timeline, the callout, the workbook strip
   and the related docs list keep the treatments their own cards settled.
   Two registers carry the structure. The masthead is the ticket's identity, set at
   the prose size in the heading weight, with the status line under it one step
   down and secondary, so the head reads as a title and its dateline rather than
   two bold paragraphs. Every open section is led by one label in the register the
   card's folds already use (small, weighted, tracked, secondary), unboxed, because
   a box around each of six sections would make a card kit of a chart. Sections sit
   one --space-lg apart, a step wider than the --space-md the blocks inside them
   keep, so the eye finds the section breaks before it reads. The callout stays the
   one loud element. The context group is one bordered box holding a row per
   section, hairlines between, each summary carrying its count or its first
   sentence, so the operator reads what is there without opening anything.
   No token or shared step changes; every value is an existing step. */
.brief-card .brief-body .brief-masthead {
  margin: 0;
  max-width: none;
  font-size: var(--font-size-prose);
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.01em;
}
.brief-card .brief-body .brief-status {
  margin: var(--space-xs) 0 0;
  max-width: none;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.brief-card .brief-body .brief-status-label {
  margin-right: var(--space-sm);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
.brief-card .brief-body > .brief-section { margin-top: var(--space-lg); }
.brief-card .brief-body .brief-section-label {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-xs);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}
/* The blocks inside a section keep the body's measure and rhythm; the msg-text rules
   that set them are child selectors on the body and no longer reach this deep. */
.brief-card .brief-section > p,
.brief-card .brief-section > ul,
.brief-card .brief-section > ol,
.brief-card .brief-section > blockquote { max-width: var(--measure-prose); margin: 0 0 var(--space-md); }
.brief-card .brief-section > table { max-width: var(--measure-wide); }
.brief-card .brief-section > :last-child { margin-bottom: 0; }
/* The server blocks carried their own top gap for the foot of the card; under a label
   the label's margin is the gap. */
.brief-card .brief-section > .brief-timeline { margin: 0; }
.brief-card .brief-section-label + .evidence-block,
.brief-card .brief-section-label + .evidence-note { margin-top: 0; }
/* The Current issue dateline (IW-472): the date of the message the issue comes from,
   beside the label on its baseline, in the status line's size and the muted tone, so
   the label keeps the register every section shares and the date reads as its
   dateline, not as a second label. Sentence case: a date is not a label. */
.brief-card .brief-body .brief-section-when {
  margin-left: var(--space-xs);
  font-size: var(--font-size-sm);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-text-muted);
}
/* A matched constraint is the one block allowed the warning tone: it governs every
   reading and every step below it. "None matched" never reaches the card (IW-472). */
.brief-card .brief-hw-body {
  max-width: var(--measure-prose);
  padding: var(--space-sm) var(--space-md);
  border-left: 3px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  background: var(--color-status-warning-bg);
  color: var(--color-text-primary);
}
.brief-card .brief-hw-body > p { margin: 0 0 var(--space-xs); }
.brief-card .brief-hw-body > :last-child { margin-bottom: 0; }
/* The context group: one box, a row per section. The rows drop the box each details
   carries page-wide (border, radius, margin, the summary's page fill) and take a
   hairline between them instead, so the six read as one index and not six cards. */
.brief-card .brief-body > .brief-context {
  margin-top: var(--space-lg);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}
.brief-card .brief-context > details {
  margin: 0;
  border: 0;
  border-radius: 0;
}
.brief-card .brief-context > details + details { border-top: 1px solid var(--color-border-subtle); }
/* The timeline's Earlier fold (IW-473) is one of these rows standing alone at the foot
   of the spine (IW-520), so it shares the row's summary, title and count by selector: a change
   to the closed-row look reaches the fold with it. */
.brief-card .brief-context > details > summary,
.brief-card .brief-timeline > .tl-earlier > summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  min-width: 0;
  background: none;
  border-radius: 0;
}
/* The title keeps the summary's register (the page-wide summary rule undoes it on
   every span) and never shrinks or wraps: beside a long gist on a phone a bare text
   run was squeezed to one letter a line. */
.brief-card .brief-context .brief-ctx-title,
.brief-card .brief-timeline > .tl-earlier .brief-ctx-title {
  flex: none;
  white-space: nowrap;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.brief-card .brief-context .brief-ctx-count,
.brief-card .brief-timeline > .tl-earlier .brief-ctx-count { color: var(--color-text-muted); }
/* The first sentence of a prose row, one line, cut at the row's end; gone once the
   row is open and the sentence is on the page. */
.brief-card .brief-context > details[open] > summary .brief-ctx-gist { display: none; }
.brief-card .brief-context .brief-ctx-gist {
  flex: 1 1 auto;
  min-width: 0;
  margin-left: var(--space-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text-muted);
}
.brief-card .brief-context .brief-ctx-body { padding: 0 var(--space-md) var(--space-md); }
.brief-card .brief-context .brief-ctx-body > table { max-width: var(--measure-wide); }
.brief-card .brief-context .brief-ctx-body > .brief-boat,
.brief-card .brief-context .brief-ctx-body > .evidence-block { margin-top: 0; }
.brief-card .brief-context .brief-ctx-body > :last-child { margin-bottom: 0; }
/* The guide link at the card's foot (IW-561). A colophon, not an action: the smallest
   size, muted, and no underline until it is pointed at, so it never competes with the
   brief it explains. */
.brief-card .brief-foot {
  margin: var(--space-md) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.brief-card .brief-foot a { color: inherit; text-decoration: none; }
.brief-card .brief-foot a:hover,
.brief-card .brief-foot a:focus-visible {
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Show boat history after the brief (IW-763). The card's closing line, above the
   guide link: what boat history would add, in the secondary tone at the prose measure,
   and under it the pill the composer's action row uses for a suggested command, since
   it does the same thing, typing /boat-history for the operator. A hairline sets it
   apart from the closed rows above, so it reads as the brief's last word, not one of
   its sections. */
.brief-card .brief-boat-offer {
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
  max-width: var(--measure-prose);
}
.brief-card .brief-boat-offer-text {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.brief-card .brief-boat-offer-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
}
.brief-card .brief-boat-offer-pill:disabled { opacity: 0.6; cursor: default; }

/* Related docs on the brief card (IW-433). A reading list, closed by default like
   Evidence: the summary carries the count, and the list opens on demand. Each entry is
   the page title as the link, where it lives as a quiet tag, the product scope in words
   when it narrows the match, and the excerpt as secondary text. The rows are set a step
   below the card's prose like the evidence tables, because this is material the brief
   cites rather than the brief. No rule or box between entries: the title link is the
   structure, and a border per reference would make a card kit of a reading list.
   The measure is capped on the entry, which keeps the card's inherited size; a cap set
   on an element at the smaller size would come out narrower than the blocks above it
   (see the measure learning in docs/solutions). */
.brief-related { margin-top: var(--space-sm); }
/* The summary takes the shared brief-section register from the `details summary` rule
   above, the same as Evidence; the count span is already exempt from its uppercase. */
.brief-related .related-count { color: var(--color-text-muted); }
/* The list and the stamp take the summary's own horizontal inset, so the entries align
   under the summary text rather than the disclosure's border. */
.brief-related .related-list { list-style: none; margin: 0; padding: var(--space-sm) var(--space-md) 0; }
.brief-related .related-doc { margin: 0 0 var(--space-sm); max-width: var(--measure-prose); }
.brief-related .related-doc:last-child { margin-bottom: 0; }
.brief-related .related-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-sm);
  font-size: var(--font-size-sm);
}
/* The title carries the entry. Linked, it takes the same accent underline as a link in
   the brief's prose; a hit with no address stays a plain title. */
.brief-related .related-title { font-weight: 500; color: var(--color-text-primary); }
.brief-related a.related-title { color: var(--color-accent); text-decoration: underline; }
.brief-related a.related-title:hover { color: var(--color-accent-hover); }
/* Where the page lives, in the tier chip's shape: outlined, no fill, one size down. */
.brief-related .related-source {
  display: inline-block;
  padding: 0 var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.brief-related .related-scope { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.brief-related .related-snippet {
  margin: 2px 0 0;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  color: var(--color-text-muted);
}
.brief-related .related-stamp {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* Without a stamp (a list of Confluence hits only) the last entry closes the box. */
.brief-related .related-list:last-child { padding-bottom: var(--space-sm); }

/* The brief's timeline (IW-434). This is the section the operator reads to understand
   the case, so it is set as a spine rather than a table, the newest event at the top
   (IW-520) because that is what a ticket is opened for: a hairline rail, the date in
   mono figures beside it, and what happened to the right. The eye runs down the dates
   and across to the event, the way a log is read, and the date column never wraps or
   competes with the text. The dates carry day and month only; the year is a marker,
   not a column. It interrupts the rail once where the year first shows and again only
   where it turns, naming the year of the rows below it, letterspaced small mono with a rule running out from it, because a
   case that has crossed a year boundary is a case that has run long, and that is the
   one fact the marker exists to carry. A folded related ticket's events sit on the same
   rail with the ticket number as a mono chip and a quieter date, so one story reads
   through. Photos sit in a row of small squares under their event: a picture the brief
   read has a solid edge, one it only listed a dashed one, so read and unread are told
   apart at a glance without a label. No token or shared step changes here; every value
   is an existing step used for this one block.
   The scanning pass (IW-522). A long ticket lost its row edges: wrapped summaries and
   photo strips ran together, and each summary started wherever its role word ended.
   So a row is now three cells (the date, who spoke, what happened), every second row
   carries a light band, and the Earlier fold's rows sit on the open rows' columns. The
   rules below say how each is done. */
.brief-card .brief-timeline {
  margin: var(--space-sm) 0 var(--space-md);
  max-width: var(--measure-wide);
}
.brief-timeline .tl-folded {
  margin-bottom: var(--space-sm);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* The list, the row and the year marker are keyed through the card. The card's body is
   assistant prose, whose own list rules (.msg-assistant .msg-text ol and li, further up)
   carry an element in the selector and so outrank a pair of classes: until IW-522 the
   prose indent and item margins were what this list actually wore, and the values set
   on .brief-timeline .tl-events never applied. Three classes outrank the prose rules
   wherever either sits in the file, and each rule sets the margin the prose rule gives. */
.brief-card .brief-timeline .tl-events {
  list-style: none;
  margin: 0;
  padding: 0;
  border-left: 1px solid var(--color-ledger-rail);
}
/* The year marker takes the row's own inset, so the figure stands over the date column
   it names and its rule runs out to the row's far edge. */
.brief-card .brief-timeline .tl-year {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
  padding: var(--space-md) 0 var(--space-xs) var(--space-md);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
}
.brief-card .brief-timeline .tl-year::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--color-border-subtle);
}
.brief-card .brief-timeline .tl-year:first-child { padding-top: 0; }
/* The Earlier fold (IW-473): events more than a month before the brief, closed in one
   row at the foot of the spine (IW-520), where reading down has gone back in time. It
   is a context row standing alone (its summary, title and count share the row rules
   above by selector), so it takes the context group's box and corner. Open, its rows
   are the spine continued, not a second spine set inside a box (IW-522): the list drops
   its own rail and the box's left edge stands in for it, which puts the date, who and
   summary columns of the folded rows on the same edges as the open rows above them.
   The last row's corners follow the box's, so a banded last row does not square off a
   rounded box. The box starts a step down from the open spine above it, which always
   holds at least the latest row. */
.brief-timeline > .tl-earlier {
  margin: var(--space-sm) 0 0;
  border-radius: var(--radius-md);
}
.brief-card .brief-timeline > .tl-earlier > .tl-events { border-left: none; }
.brief-card .brief-timeline > .tl-earlier > .tl-events > .tl-event:last-child {
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}
/* One row (IW-522): three cells on one baseline, the date, who spoke, and what happened.
   Both narrow columns are widths in px, sized for the longest thing each holds
   ("30 Sep" in the mono face, "Internal note" in the label's), so every summary starts
   on one edge whoever spoke and a wrapped summary hangs under itself. The who column
   may grow past its width for one row only: a kind the server has no label for arrives
   as its own token (automation_note), which has no space to wrap at, and a row whose
   summary starts a little late reads better than a label broken mid word. The padding is the
   same above and below, because a row is now a unit with an edge of its own.
   The band: every second event carries a wash of the text colour, so a row with a
   wrapped summary and a strip of photos still reads as one thing and the next row as
   another. It is the ink at a few parts in a hundred rather than a surface token,
   which keeps it a hint on the card in both themes and on a superseded card, whose
   surface is the page's; a surface tone that shows on one of those vanishes on another.
   The count is of events only, so a year marker between two rows does not shift the
   alternation, and it starts clean, so the newest row is never the tinted one. */
.brief-card .brief-timeline .tl-event {
  display: grid;
  grid-template-columns: 48px minmax(84px, max-content) minmax(0, 1fr);
  column-gap: var(--space-md);
  align-items: baseline;
  margin: 0;
  padding: var(--space-sm) var(--space-md);
}
.brief-card .brief-timeline .tl-event:nth-child(even of .tl-event) {
  background: color-mix(in srgb, var(--color-text-primary) 4%, transparent);
}
.brief-timeline .tl-date {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--color-text-secondary);
}
.brief-timeline .tl-related .tl-date { color: var(--color-text-muted); }
.brief-timeline .tl-body { min-width: 0; }
.brief-timeline .tl-line {
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-primary);
}
/* The line always holds a line, so the row always has a first baseline to set the date
   and who against. A message that was photos only has no summary and no excerpt, and
   an empty line has no baseline: the row would take one from the foot of the first
   thumbnail and the date would sit beside the bottom of the picture. */
.brief-timeline .tl-line::before { content: "\200b"; }
/* Who spoke is a label, not a sentence: small, weighted, in a column of its own. The
   customer's rows carry the primary tone because the case is their account of it; a
   support reply and an internal note step back one tone each. */
.brief-timeline .tl-who {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.brief-timeline .tl-customer .tl-who { color: var(--color-text-primary); }
.brief-timeline .tl-internal_note .tl-who { color: var(--color-text-muted); }
.brief-timeline .tl-internal_note .tl-what { color: var(--color-text-secondary); }
/* Core's excerpt stands in only when no fragment arrived; it is the message's own
   words, so it takes the quotation treatment the body gives someone else's voice. */
.brief-timeline .tl-excerpt { font-style: italic; color: var(--color-text-secondary); }
.brief-timeline .tl-ticket {
  display: inline-block;
  margin-right: var(--space-sm);
  padding: 0 var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
/* Who wrote another ticket's event on a Boat timeline (IW-563): owner, previous owner,
   partner, Integrel. It reads with the ticket marker as one attribution, "#3001 Partner",
   so it takes the marker's size and tone without its frame, and the fragment follows. */
.brief-timeline .tl-author {
  margin-right: var(--space-sm);
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-text-secondary);
}
/* An RMA or Sales Quote Request card's row (IW-511): a ledger entry, not a message. Who
   is the kind of card, and the line is the card's key and what happened to it, so the
   key is set as an identifier, in the mono face the date and the ticket marker already
   use, and it is the row's link to the card in Jira (it takes the card's link colour
   from the prose rule above, so it reads as a link without a rule of its own). The
   line steps back one tone: the row records that parts moved, and the messages around
   it carry the case. */
.brief-timeline .tl-card-line { color: var(--color-text-secondary); }
.brief-timeline .tl-card-key {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Fragments alone (no server rows): undated and unattributed, so the two narrow
   columns would be two empty gutters. The row is its text and not a grid at all, at any
   width, so the tight step's columns below have nothing here to act on. */
.brief-card .brief-timeline-fragments .tl-event { display: block; }
/* What rides on a row (its photos, an earlier capture's readings) sits in the summary's
   column one step under the line above it, the same step for each, so a row with
   photos and a row without share every edge but the foot. */
.brief-timeline .tl-media {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}
.brief-timeline .tl-thumb {
  display: block;
  margin: 0;
  padding: 0;
  width: 56px;
  height: 56px;
  overflow: hidden;
  border: 1px dashed var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
  /* A button since IW-435 (it opens the viewer): the reset above takes the browser's
     own button padding and font. The cursor says what the press does; the edge does not change on hover, because the
     solid and dashed edges already carry a meaning (read, not read) that a hover
     treatment would talk over. */
  font: inherit;
  color: inherit;
  cursor: zoom-in;
}
.brief-timeline .tl-thumb-read { border-style: solid; border-color: var(--color-text-muted); }
.brief-timeline .tl-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Photos a message only sent again (IW-675): one line under the row's own photos, in
   place of thumbnails the operator has already seen on the row that first sent them.
   It is a note about the message, so it takes the small size and second tone of the
   row's other marginal words (who spoke, the ticket marker) and sits the same step
   under what is above it as the photo strip does. It is also the control that shows
   the copies in place, so it is the page's own disclosure with its register undone,
   like an earlier capture's: no box, no fill, sentence case, the marker one tone
   quieter than the words. The summary is only as wide as its words, so the pointer
   does not claim the rest of the row. Open, the copies are an ordinary photo strip. */
.brief-timeline .tl-resent {
  margin: var(--space-sm) 0 0;
  border: none;
  border-radius: 0;
}
.brief-timeline .tl-resent > summary {
  display: block;
  width: fit-content;
  max-width: 100%;
  padding: 0;
  background: none;
  border-radius: 0;
  font-size: var(--font-size-xs);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-text-secondary);
  cursor: pointer;
}
.brief-timeline .tl-resent > summary::before { color: var(--color-text-muted); }
/* The tight pane (a phone): three columns leave the summary a strip a few words wide,
   so the date and who share the row's first line and the summary takes the whole width
   under them. An undated row gives up the date's cell, so who starts at the row's edge.
   The summary is no longer on the date's line, so it needs no line of its own to lend a
   baseline: a photos-only row puts its photos straight under the date and who. */
@container pane (max-width: 560px) {
  .brief-card .brief-timeline .tl-event {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--space-sm);
  }
  .brief-timeline .tl-date:empty { display: none; }
  .brief-timeline .tl-body { grid-column: 1 / -1; }
  .brief-timeline .tl-line::before { content: none; }
}
/* A clip or a file on a row (IW-524) opens in a new tab, so it is a link, not a
   picture: a low chip set beside the thumbnails, centred on them, with the kind as a
   small mono marker (the register of the ticket marker and the card key) and the name
   in the row's own face. It has no fill of its own, so it reads on a banded row, in
   either theme and on a superseded card, and no hover tone, which the block keeps off
   everything; the pointer and the tooltip say it opens. The edge is
   solid: dashed already means a photo the brief did not read, and the brief reads no
   file. A chip whose attachment would not open takes the fault tone on its edge and
   says why in a short line beside it, in the tone the page's other failed lines use;
   the chip stays a link, so a second press tries again. A thumbnail that failed to load still leaves the row (IW-521). The chip's
   selector outranks the chat's prose link rule (.msg-assistant .msg-text a), which
   would otherwise colour and underline it like a link in a sentence. */
.brief-card .brief-timeline a.tl-chip {
  display: inline-flex;
  align-items: baseline;
  align-self: center;
  gap: var(--space-xs);
  max-width: 100%;
  min-width: 0;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  line-height: 1.4;
  color: var(--color-text-primary);
  text-decoration: none;
}
.brief-timeline .tl-chip-kind {
  font-family: var(--font-mono);
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.brief-timeline .tl-chip-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A chip waiting on its link (IW-658): the pointer says the press was taken, for the
   second or so the read of the thread takes before a clip's player or a file's tab. */
.brief-card .brief-timeline a.tl-chip[data-opening] { cursor: progress; }
.brief-card .brief-timeline a.tl-chip-missing { border-color: var(--color-status-fault); }
.brief-timeline .tl-chip-note {
  align-self: center;
  font-size: var(--font-size-xs);
  color: var(--color-status-fault);
}
/* A clip's tile (IW-930). A video sits in the strip as a tile the size of a photo
   thumbnail, with a play badge over its centre, so a glance tells a clip from a photo
   and the press that plays it is the press a thumbnail takes. The badge is the page's
   scrim tone under an inverse triangle, which reads over any frame and on the plain
   ground a clip with no frame keeps (the script marks that tile tl-thumb-blank and
   takes the picture out; the tile's own fill is the ground). The edge keeps the photo
   rule, solid for a clip the brief read and dashed for one it only listed. The badge
   is drawn in the markup, so the sheet sets only its size, its place and its tones.
   Nothing moves on hover, as on every thumbnail. A tile waiting on its link and one
   whose clip would not open take the chip's cursor and edge. */
.brief-timeline .tl-thumb-clip {
  position: relative;
  cursor: pointer;
}
.brief-timeline .tl-thumb-clip .tl-play {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  pointer-events: none;
}
.brief-timeline .tl-thumb-clip .tl-play circle { fill: var(--color-scrim); }
.brief-timeline .tl-thumb-clip .tl-play path { fill: var(--color-text-inverse); }
.brief-timeline .tl-thumb-clip[data-opening] { cursor: progress; }
.brief-timeline .tl-thumb-clip.tl-chip-missing { border-color: var(--color-status-fault); }
/* An earlier capture's readings on its row (IW-436). The row already says a photo
   arrived; this says what the judge read from it, closed, in the same small register as
   the event line so the timeline stays a timeline and not a stack of tables. The summary
   is the table's own head line led by one weighted word, and it overrides the page-wide
   summary treatment (uppercase, padded, on the page tone) because that register is for
   card sections and this is one line inside a row. A count of cells the judge flagged
   sits at the end in the fault tone, so a capture that had something wrong is told from
   one that did not before it is opened. Open, the matrices take the same block they take
   at the top of the card, set in the row's summary column (the whole row at the tight
   step), so they have that column's width and scroll sideways past it. */
.brief-timeline .tl-evidence {
  margin: var(--space-sm) 0 0;
  /* Not a card section: the page-wide details box (border, radius) is undone here,
     because a box inside a row would draw a card inside the spine. */
  border: none;
  border-radius: 0;
}
.brief-timeline .tl-evidence > summary {
  display: block;
  padding: 0;
  background: none;
  border-radius: 0;
  font-size: var(--font-size-sm);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-text-primary);
  cursor: pointer;
}
/* The marker itself comes from the details rules near the top of the file, keyed on
   this disclosure's own state; only its tone is set here, one step quieter than the
   head line it leads. */
.brief-timeline .tl-evidence > summary::before { color: var(--color-text-muted); }
.brief-timeline .tl-evidence > summary span { font-size: inherit; }
.brief-timeline .tl-evidence-label {
  margin-right: var(--space-sm);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.brief-timeline .tl-evidence-flagged {
  margin-left: var(--space-sm);
  /* The count and its words stay together: "1" at a line's end and "outside
     expectation" on the next reads as a stray figure. */
  white-space: nowrap;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-status-fault);
}
.brief-timeline .tl-evidence > .evidence-block { margin-top: var(--space-xs); }

/* A superseded brief (IW-174). Running the brief again re-renders the whole card, so
   the earlier one folds behind this disclosure the moment the new one lands: two copies
   of the same two thirds never sit expanded on the page at once. The folded card reads
   as history, quieter than the live one, and its summary reopens it.
   Recessed against the page rather than raised off it: --color-bg-raised equals
   --color-bg-surface in the light theme (both #ffffff), so surface would have read as
   no change at all there. --color-bg-page sits below raised in BOTH themes. */
.brief-card.brief-superseded {
  background: var(--color-bg-page);
  border-style: dashed;
}
.brief-fold > summary {
  cursor: pointer;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
/* Go to the current brief (IW-764). A text control at the summary's end, in the
   register View original uses: the accent is what makes three words read as a control,
   the underline comes on reach. It keeps the summary's size so the line stays one
   quiet line, and it may wrap under the title on a phone rather than squeeze it. */
.brief-fold > summary .brief-jump {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-weight: 500;
  color: var(--color-accent);
  cursor: pointer;
  white-space: nowrap;
}
.brief-fold > summary .brief-jump:hover,
.brief-fold > summary .brief-jump:focus-visible {
  color: var(--color-accent-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Where the jump lands: a ring in the accent for a moment, so the eye finds the card
   after a long scroll. An outline, so nothing on the card moves or reflows; it comes
   and goes with no animation, which also suits reduced motion. */
.brief-card.brief-landed {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Out of date (IW-764): the newest brief was built before something changed on the
   case. A note, not a warning: the brief is still the only one and most of it is still
   true, so the strip takes no status colour and never dims the card. It sits under the
   title, recessed into the card on the page's own ground with a muted rule at its left,
   the same shape as the asked block but in neutral tones, and its button is the plain
   outline button in the text's own colour, not the accent: rebuilding is the operator's
   call, not the suggested next move, and not the slash command chip either, whose mono
   face would make a sentence read as code. btn-small brings its own disabled state. */
.brief-card .brief-stale {
  margin: var(--space-xs) 0 var(--space-md);
  padding: var(--space-sm) var(--space-md);
  max-width: var(--measure-prose);
  border-left: 3px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-page);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
}
.brief-card .brief-stale-text {
  flex: 1 1 18em;
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.brief-card .brief-stale-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
}
/* Judged P-code evidence tables on the brief card (IW-55). Server-rendered data:
   a flagged miss reads loud, a recognized benign condition reads calm, a row the
   layer had nothing to say about stays quiet. */
.evidence-block { margin-top: var(--space-sm); }
.evidence-head { font-weight: 600; font-size: var(--font-size-sm); }
.evidence-meta {
  margin-left: var(--space-sm);
  font-weight: 400;
  color: var(--color-text-muted);
}
/* View original (IW-435): the way from a doubtful reading back to the picture it was
   read from. A text control at the head's end rather than a bordered button, because
   the head is one line of label text and a boxed control there would outweigh the
   table under it. The accent is the product's interactive colour, which is what makes
   two words at the end of a label read as a control; the underline appears on hover
   and focus only, so the line stays quiet until the operator reaches for it. */
.evidence-head .evidence-view-original {
  margin-left: var(--space-sm);
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-accent);
  cursor: pointer;
}
.evidence-head .evidence-view-original:hover,
.evidence-head .evidence-view-original:focus-visible {
  color: var(--color-accent-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Where the rest went (IW-436): one muted line under the latest capture saying how many
   earlier captures sit on the timeline, in the note's size without the note's bar, since
   it is a pointer and not a warning. */
.evidence-earlier {
  margin-top: var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
/* A prior ticket's readings from its History row (IW-436). The control sits in the
   ticket cell after the number, in the same text-control register as View original: two
   words in the accent, underlined only when reached for. A row with nothing to show gets
   the same control in the muted tone, so the absence is opened and read where the
   reading would have been rather than guessed at. The readings open in a row beneath,
   as a block set in from the table's own left edge, led by one line saying which brief
   they came from and the server's note on what was and was not read. */
.brief-body table .history-readings {
  margin-left: var(--space-sm);
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-accent);
  white-space: nowrap;
  cursor: pointer;
}
.brief-body table .history-readings-none { color: var(--color-text-muted); }
.brief-body table .history-readings:hover,
.brief-body table .history-readings:focus-visible {
  color: var(--color-accent-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.brief-body table .history-readings:disabled { cursor: default; opacity: 0.6; }
/* A boat ticket the brief did not draw (IW-563) is offered from its History row in the
   same register as the readings control beside it: a text control, the accent at rest.
   Its label is four words, so unlike Readings it may wrap: held to one line it set the
   ticket column's width, and on a phone the other columns fell to a letter a line. */
.brief-body table .history-add {
  margin-left: var(--space-sm);
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-accent);
  text-align: left;
  cursor: pointer;
}
.brief-body table .history-add:hover,
.brief-body table .history-add:focus-visible {
  color: var(--color-accent-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.brief-body table .history-add:disabled { cursor: default; opacity: 0.6; }
.brief-body table .history-readings-row > td {
  padding: var(--space-xs) 0 var(--space-md);
  font-variant-numeric: normal;
}
.brief-body .prior-evidence { max-width: var(--measure-wide); }
.brief-body .prior-evidence-line {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.brief-body .prior-evidence-note {
  margin-top: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.evidence-table {
  width: 100%;
  margin-top: var(--space-xs);
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.evidence-table th, .evidence-table td {
  border: 1px solid var(--color-border-subtle);
  padding: 2px var(--space-sm);
  text-align: left;
  vertical-align: top;
}
/* A header is a short label (Code, a device such as FC BB14) and never wraps: the
   page-wide overflow-wrap: anywhere split "Code" mid-word once the table sat inside a
   History row (IW-436), and a device label split across two lines reads as two units. */
.evidence-table th { color: var(--color-text-secondary); font-weight: 600; white-space: nowrap; }
/* A judged table drawn inside the brief body (an earlier capture on its timeline row, a
   prior ticket's readings under its History row, IW-436) sits inside the prose-table
   register above, which sets mono uppercase headers and horizontal rules for the model's
   own tables. A judged matrix is instrument output of a different kind, one cell per
   device with a flag on it, and it keeps its grid and its own head wherever it is drawn,
   so a reading reads the same at the top of the card and under a row. */
.msg-assistant .msg-text .evidence-table { margin: var(--space-xs) 0 0; width: 100%; }
.msg-assistant .msg-text .evidence-table th,
.msg-assistant .msg-text .evidence-table td {
  border: 1px solid var(--color-border-subtle);
  padding: 2px var(--space-sm);
  font-family: inherit;
  font-size: var(--font-size-sm);
  letter-spacing: normal;
  text-transform: none;
}
.msg-assistant .msg-text .evidence-table th { color: var(--color-text-secondary); font-weight: 600; }
.evidence-scroll { overflow-x: auto; }
.evidence-class {
  margin-top: var(--space-sm);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
/* The class tables as a workbook (IW-446): one tab per device class over one sheet at a
   time. The tabs take the class label's own register (secondary, weighted) so they read
   as the labels they replace, and the open one gets the product's underline tab, the
   registry's accent rule under the text, without a weight change that would shift the
   strip. On a narrow screen (a phone, or a capture set in under a timeline row) the
   tabs wrap to a second line rather than scroll: a strip that scrolled sideways hid
   the open tab and its count, the one tab the operator was sent to. The rule under the
   strip is an inset shadow so it sits under the last line and each tab's own underline
   paints over it. The count is the one loud mark: it wears the fault fill the flagged
   cells wear, so the number reads as those cells gathered, and a clean sheet's tab
   carries nothing. A closed sheet is hidden until found, which keeps its box but
   skips and collapses its contents (plain hidden where the value is not supported),
   so the sheet itself carries no margin, border or padding that would leave a sliver
   behind. */
.evidence-sheets { margin-top: var(--space-sm); }
.evidence-sheet-tabs {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-xs);
  box-shadow: inset 0 -1px 0 var(--color-border-subtle);
}
.evidence-sheet-tab {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: none;
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
  white-space: nowrap;
  cursor: pointer;
}
.evidence-sheet-tab:hover { color: var(--color-text-primary); }
.evidence-sheet-tab[aria-selected="true"] {
  color: var(--color-text-primary);
  border-bottom-color: var(--color-accent);
}
.evidence-sheet-tab:focus-visible { outline-offset: -2px; }
/* Positioned for the words a screen reader hears after the figure (.visually-hidden). */
.evidence-sheet-count {
  position: relative;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-sm);
  background: var(--color-status-fault-bg);
  color: var(--color-status-fault);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}
.evidence-sheet:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}
.evidence-meaning { color: var(--color-text-muted); max-width: 26em; }
/* Column sizing in the brief's diagnostic table (IW-766). A table lays its columns out
   by giving the slack to each in proportion to what it would like to be, and a column
   with no minimum can be squeezed to its narrowest word, which the page-wide
   overflow-wrap lets be a single letter. That is how Parameter and Meaning came to
   break mid-word and every row to run four lines tall beside a wide reading column and
   an Expected column holding its reserve. So the two text columns each keep a reading
   width and break only between words, the reading columns take their content and no
   more (no-wrap with width: 1%, the table idiom for that), and what is left goes to
   Meaning up to its maximum, then to Expected. When the pane cannot hold every column
   at its minimum the table scrolls sideways inside .evidence-scroll, as IW-661
   intended, rather than crushing the text. The Expectations page draws the same table
   from its own template and is left as it was: .xp-table keeps the old sizing. */
.evidence-table:not(.xp-table) .evidence-param { min-width: 8em; overflow-wrap: normal; }
.evidence-table:not(.xp-table) .evidence-meaning { min-width: 12em; overflow-wrap: normal; }
.evidence-table:not(.xp-table) .evidence-value { width: 1%; white-space: nowrap; }
/* The decoded meaning under a reading is a sentence, so it wraps inside the column
   rather than stretching it, and holds a width of its own so it never folds to one word
   a line. */
.evidence-table:not(.xp-table) .evidence-decoded { white-space: normal; min-width: 10em; }
/* Decoded enum/bitmask reading (IW-64): the meaning sits under the raw value. */
.evidence-decoded {
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  max-width: 16em;
}
/* Per-device cell flags (IW-61): loud miss, calm named condition, quiet match. */
.evidence-cell-outside { background: var(--color-status-fault-bg); color: var(--color-status-fault); font-weight: 600; }
.evidence-cell-condition { background: var(--color-status-waiting-bg); color: var(--color-status-waiting); }
.evidence-cell-ok { color: var(--color-status-resolved); }
/* A judged cell says its verdict in hidden words (IW-659). They are out of flow, so
   they need a positioned ancestor (.visually-hidden), and it is the scroller rather
   than each cell: a positioned cell with a fill can paint over a collapsed border, and
   words anchored outside the scroller would widen the page when the table scrolls.
   They stay out of a selection so a copied table carries the readings alone. */
.evidence-scroll { position: relative; }
.evidence-spoken { -webkit-user-select: none; user-select: none; }
/* The Expected cell (IW-659): the value on file, its tier chip, and a tick when every
   judged reading on the row sat inside it. The tick takes the resolved green the
   matching cells wear, at the chip's size, so "checked and fine" reads as one quiet
   mark and never competes with a fault fill. A row with nothing on file is an empty
   cell. The text is its own span so a control can sit beside it. */
.evidence-expected-text { overflow-wrap: normal; }
/* A value with its conditions in words runs to a sentence (IW-661), so on a narrow
   screen the column keeps a reading width and the table scrolls sideways, rather than
   the cell folding the sentence into a tall column of single words. */
.evidence-expected { min-width: 14rem; }
.evidence-tick {
  margin-left: 2px;
  color: var(--color-status-resolved);
  font-size: var(--font-size-xs);
  font-weight: 600;
  vertical-align: middle;
}
/* Readings no operating state could judge (IW-659): the note's own bar, said once
   between the head line and the table, with the two words that name it weighted. */
.evidence-unjudged { margin-bottom: var(--space-xs); }
.evidence-unjudged-label { font-weight: 600; color: var(--color-text-primary); }
/* The conditions line (IW-661): what Integra could tell of this capture, under the
   head and above the table. It reports what was read, not a problem, so it carries
   no warning bar: three runs in the muted register, each led by its weighted label.
   Could not tell gets the waiting tone on its label, the one run that asks for a
   better capture. */
.evidence-conditions {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-md);
  margin: 2px 0 var(--space-xs);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.evidence-conditions-label { font-weight: 600; color: var(--color-text-secondary); }
.evidence-conditions-untold .evidence-conditions-label { color: var(--color-status-waiting); }
/* A cell the photos of one round read differently (IW-474): one line per reading, each
   followed by the photo it came from in the muted register the decoded meaning uses.
   The waiting-colour rule down the left edge marks the cell as changed between shots
   without competing with a fault fill, which it sits beside rather than replaces. */
.evidence-cell-split { box-shadow: inset 2px 0 0 var(--color-status-waiting); }
.evidence-reading { white-space: nowrap; }
.evidence-reading + .evidence-reading { margin-top: 2px; }
.evidence-reading-other { color: var(--color-text-primary); font-weight: 400; }
.evidence-reading-photo {
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  font-weight: 400;
}
.tier-chip {
  display: inline-block;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border-subtle);
  vertical-align: middle;
}
.tier-documented { color: var(--color-status-resolved); border-color: var(--color-status-resolved); }
.tier-confirmed { color: var(--color-status-waiting); border-color: var(--color-status-waiting); }
/* An unproven fix (IW-512) sits in the same list as a confirmed path, so the
   chip carries the difference: the broken outline says nothing closed this
   one, in the secondary text colour rather than a status tone, because it is
   neither a warning nor a result. */
.tier-unproven { color: var(--color-text-secondary); border-style: dashed; border-color: var(--color-text-muted); }
/* A known condition (IW-765): a value on file that is expected only when something is
   wrong, such as the ~105V ghost reading, so it must not read as a healthy one. The
   mark sits beside the tier chip, which stays (documented or confirmed is still true of
   it), in the warning tone the known-error chip uses; the value takes the warning text
   colour, and the entry wears the warning-tone left rule the split cell uses. A normal
   value draws none of this. The same three classes serve the Expectations row, the review
   card, the edit dialog and the brief's evidence table. */
.known-mark {
  display: inline-block;
  padding: 0 var(--space-xs);
  border: 1px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  background: var(--color-status-warning-bg);
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-status-warning);
  vertical-align: middle;
}
.known-value { color: var(--color-status-warning); }
.known-entry { box-shadow: inset 2px 0 0 var(--color-status-warning); padding-left: var(--space-sm); }
/* Proof plan (IW-79, IW-289): a definition line, then one card per hypothesis
   on a single surface, then the shared check-existing-media line once. */
.proofplan-definition,
.proofplan-shared {
  margin-top: var(--space-xs);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.proofplan-hyp {
  margin-top: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-surface);
  font-size: var(--font-size-sm);
}
.proofplan-cause { font-weight: 600; color: var(--color-text-primary); }
.proofplan-test,
.proofplan-variant,
.proofplan-links {
  margin-top: var(--space-xs);
  color: var(--color-text-secondary);
}
.proofplan-variant { color: var(--color-text-muted); }
/* How an unproven fix was established (IW-512): the label that keeps it from
   reading as proof, so it takes the primary text colour of the line above. */
.proofplan-established { margin-top: var(--space-xs); color: var(--color-text-primary); }
/* The picture kept with a proven fix (IW-333), shown to the operator. */
.proofplan-figure { margin: var(--space-xs) 0 0; }
.proofplan-figure img {
  display: block;
  max-width: 100%;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}
.proofplan-figure figcaption {
  margin-top: var(--space-xs);
  color: var(--color-text-muted);
}
.proofplan-effort {
  display: inline-block;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  background: var(--color-bg-hover);
  vertical-align: middle;
}
/* Known errors (IW-477): the resolved-on-a-workaround records that matched
   this ticket. Drawn on the proof plan's surface so the two read as one block
   of precedent, and kept apart from a hypothesis on purpose: the chip is the
   warning tone rather than a tier colour, the left rule marks the entry as
   caution, and there is no test line, because nothing here was proven. */
.knownerror-head { margin-top: var(--space-md); }
.knownerror-entry {
  margin-top: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-status-warning);
  border-radius: var(--radius-md);
  background: var(--color-bg-surface);
  font-size: var(--font-size-sm);
}
.knownerror-symptom { font-weight: 600; color: var(--color-text-primary); }
.knownerror-chip {
  display: inline-block;
  padding: 0 var(--space-xs);
  border: 1px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-status-warning);
  vertical-align: middle;
}
.knownerror-product {
  margin-left: var(--space-xs);
  font-weight: 400;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.knownerror-line {
  margin-top: var(--space-xs);
  color: var(--color-text-secondary);
}
.knownerror-key {
  font-weight: 600;
  color: var(--color-text-primary);
}
.knownerror-key::after { content: ":"; }
/* Team answers (IW-513): a customer's question the team answered before. On
   the same surface as the known errors so the precedent reads as one block,
   but in the product's own accent rather than a status tone: warning would say
   caution and resolved would say fixed, and a team answer is neither. The
   question leads; the answer is the content, so it reads as body text. */
.teamanswer-head { margin-top: var(--space-md); }
.teamanswer-entry {
  margin-top: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: var(--color-bg-surface);
  font-size: var(--font-size-sm);
}
.teamanswer-question { font-weight: 600; color: var(--color-text-primary); }
.teamanswer-chip {
  display: inline-block;
  padding: 0 var(--space-xs);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-accent);
  vertical-align: middle;
}
.teamanswer-product {
  margin-left: var(--space-xs);
  font-weight: 400;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.teamanswer-answer {
  margin-top: var(--space-xs);
  color: var(--color-text-primary);
  line-height: 1.5;
}
.evidence-note {
  margin-top: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  border-left: 3px solid var(--color-status-warning);
  background: var(--color-status-warning-bg);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* The photo viewer (IW-435). An operator opens a customer's phone photo of a
   diagnostic screen to check a value the table read from it, so the picture is the
   whole point and the one thing on the layer that is allowed to be loud. Everything
   else is set to disappear: a scrim over the page (the workbench dimmed, not a modal
   kit), the photograph centred at up to the viewport less a margin, square-cornered
   because it is a photograph and not a card, no frame and no shadow. The caption
   under it is the timeline's own row lifted off the card, the file name in the mono
   figures the dates are set in, the sent time beside it in the running face, and the
   position in the set at the far end in tabular figures, so the layer reads as part
   of the brief rather than as a separate product. Previous and next are wide, quiet
   hit areas down each side that brighten on hover; at an end the control fades rather
   than wrapping, so the edges of the set are edges. Close sits top right. One fade on
   the shared motion tokens, and none when the operator asked for none. Text on the
   scrim takes the inverse text role, which is light in both themes because the scrim
   is dark in both. Stacks above the drawer (30) and the header menu (40). */
.photo-viewer {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  background: var(--color-scrim);
  color: var(--color-text-inverse);
}
.photo-viewer[hidden] { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .photo-viewer { animation: photo-viewer-in var(--motion-enter) var(--ease-out); }
}
@keyframes photo-viewer-in {
  from { opacity: 0; }
}
.photo-viewer-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: var(--space-xl) 72px var(--space-md);
}
.photo-viewer-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* The thumbnail draws first and the original replaces it; the code tone fills the
     frame between the two so nothing on the scrim flashes white. */
  background: var(--color-bg-code);
}
/* The original could not be fetched and the thumbnail is what is shown: the dashed
   edge the timeline uses for a picture that was only listed, so the same mark means
   the same thing in both places. */
.photo-viewer-degraded .photo-viewer-img { outline: 1px dashed var(--color-text-inverse); }
/* Nothing could be fetched at all: no frame, the caption carries the reason. */
.photo-viewer-missing .photo-viewer-img { display: none; }
.photo-viewer-step,
.photo-viewer-close {
  position: absolute;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text-inverse);
  font-family: var(--font-sans);
  line-height: 1;
  cursor: pointer;
  opacity: 0.6;
}
.photo-viewer-step:hover,
.photo-viewer-close:hover,
.photo-viewer-step:focus-visible,
.photo-viewer-close:focus-visible { opacity: 1; }
.photo-viewer-step {
  top: 0;
  bottom: 56px;
  width: 72px;
  font-size: 44px;
}
.photo-viewer-prev { left: 0; }
.photo-viewer-next { right: 0; }
.photo-viewer-step:disabled { opacity: 0.15; cursor: default; }
.photo-viewer-close {
  top: var(--space-sm);
  right: var(--space-sm);
  width: 40px;
  height: 40px;
  font-size: 28px;
}
.photo-viewer-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm) var(--space-md);
  padding: 0 72px var(--space-lg);
  font-size: var(--font-size-sm);
}
/* What the picture is in words, never its filename (IW-827), so the body face. */
.photo-viewer-name {
  font-weight: 600;
  overflow-wrap: anywhere;
}
.photo-viewer-sent,
.photo-viewer-note { opacity: 0.75; }
.photo-viewer-note:empty { display: none; }
.photo-viewer-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}

/* The clip player (IW-658). The photo viewer's sibling, on the same layer and the same
   scrim, so a customer's clip opens the way their photo does: the video centred and
   square-cornered, nothing framing it, the timeline's row lifted off the card as the
   caption (the file name in the mono figures, the sent time in the running face) and
   Close top right. It has no previous and next, so it gives the side gutters the
   viewer keeps for them back to the video, down to a hair on a phone, where a clip
   shot upright wants every pixel of the width. The transport is the browser's own,
   untouched: an operator already knows it, and it is the part that streams. The one
   line the player ever sets in the stage is for a clip that will not play here, in
   the running face at reading size, with what the operator can do about it beside it
   as plain underlined text (Try again, the download), because at that moment the
   sentence and those two are the whole surface. The caption's note is for the short
   wait while a fresh link is fetched. */
.clip-player {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  background: var(--color-scrim);
  color: var(--color-text-inverse);
}
.clip-player[hidden],
.clip-player [hidden] { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .clip-player { animation: photo-viewer-in var(--motion-enter) var(--ease-out); }
}
/* The stage is a size container, so the video can be sized against the room it has
   in both directions at once. */
.clip-player-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  min-height: 0;
  /* The side gutter scales with the window between a hair and the page's wide step,
     so a phone gives the clip its width and no window query is needed for it. */
  margin: 56px clamp(var(--space-xs), 1.7vw, var(--space-xl)) var(--space-md);
  container-type: size;
}
/* The largest box of the clip's own shape that fits the stage. The script writes the
   shape onto the element (these two declarations with the clip's ratio, width over
   height) once the clip says it; until then the box is this
   widescreen frame, not the 300 by 150 a bare video element starts at. Because
   the element is the picture and nothing more, what lies beside an upright clip is
   the scrim, and a press there closes the player as it does beside a photo. */
.clip-player-video {
  display: block;
  width: min(100cqw, calc(100cqh * 1.7778));
  aspect-ratio: 1.7778;
}
/* A clip with sound and no picture: the transport alone, under the line that says so. */
.clip-player-sound .clip-player-video {
  width: min(100cqw, 480px);
  height: 54px;
  aspect-ratio: auto;
}
.clip-player-fallback {
  max-width: 34em;
  margin: 0;
  font-size: var(--font-size-md);
  line-height: 1.5;
  text-align: center;
}
.clip-player-retry,
.clip-player-download {
  margin-left: var(--space-xs);
  color: inherit;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  white-space: nowrap;
}
.clip-player-retry {
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}
.clip-player-close {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  width: 40px;
  height: 40px;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text-inverse);
  font-family: var(--font-sans);
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.6;
}
.clip-player-close:hover,
.clip-player-close:focus-visible { opacity: 1; }
.clip-player-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm) var(--space-md);
  padding: 0 clamp(var(--space-md), 1.7vw, var(--space-xl)) var(--space-lg);
  font-size: var(--font-size-sm);
}
/* What the picture is in words, never its filename (IW-827), so the body face. */
.clip-player-name {
  font-weight: 600;
  overflow-wrap: anywhere;
}
.clip-player-sent,
.clip-player-note { opacity: 0.75; }
.clip-player-note:empty { display: none; }

/* Completeness readout on the brief card (IW-74, IW-83). Server-rendered
   verdict: complete reads quiet, incomplete reads loud, not computed and not
   applicable read muted, error (IW-222) reads as a warning; the as-of line reuses the evidence-note styling with
   a loud stale marker. renderCompleteness builds this chip's class as
   completeness-${status}, so every member of Core's READOUT_STATUSES needs a
   rule here or its chip renders unstyled. */
.completeness-chip {
  display: inline-block;
  margin-left: var(--space-sm);
  padding: 0 var(--space-xs);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  border: 1px solid var(--color-border-subtle);
  vertical-align: middle;
}
.completeness-complete { color: var(--color-status-resolved); border-color: var(--color-status-resolved); }
.completeness-incomplete { color: var(--color-status-fault); border-color: var(--color-status-fault); }
.completeness-not_computed { color: var(--color-text-muted); }
.completeness-not_applicable { color: var(--color-text-muted); }
.completeness-error { color: var(--color-status-warning); border-color: var(--color-status-warning); }
.completeness-asof-stale {
  border-left-color: var(--color-status-fault);
  background: var(--color-status-fault-bg);
}
.completeness-item, .completeness-gap {
  margin-top: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.completeness-status { font-weight: 600; }
.completeness-item-present .completeness-status { color: var(--color-status-resolved); }
.completeness-item-missing .completeness-status,
.completeness-item-illegible .completeness-status { color: var(--color-status-fault); }
.completeness-gap-kind { font-weight: 600; color: var(--color-status-warning); }
.completeness-line {
  margin-top: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* The About this boat panel's one row (IW-317, IW-357): label, control, the button
   that commits it, and a quiet confirmed mark, on one line. All eight fields use
   it, the five text boxes and the three selects alike, so the three blocks read
   as one column of controls and one column of buttons. A grid rather than a flex
   row, because the columns have to line up ACROSS rows and across blocks. The row
   sets the label's font size itself, so the label column's em and the note's
   indent below are measured in the same unit and land on the same edge. */
.boat-block { margin-bottom: var(--space-sm); }
.boat-field {
  display: grid;
  /* Fixed tracks for the button and the mark. Each row is its own grid, so an
     auto track is sized from that row's own content and the columns stagger
     as soon as the pane is too narrow to give every control its full width. */
  grid-template-columns: 10em minmax(6rem, 22rem) 5em 5em;
  font-size: var(--font-size-sm);
  justify-content: start;
  align-items: center;
  column-gap: var(--space-sm);
  margin-top: var(--space-xs);
}
/* One step darker than the help lines below (IW-359). Muted is the lightest text
   tone there is, so a label set in it left nothing for a hint to sit under. */
.boat-label {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}
/* Inputs and selects both. The width comes from the row's column. Scoped to the
   row because the form default is input[type=text], which outranks a bare class:
   without the second class the text boxes kept the default padding and stood a
   third taller than the selects beside them. */
.boat-field .boat-input {
  width: 100%;
  /* A native select draws 24px tall at this size and a text box 22px, so both
     take the taller as a floor and every row is one height. */
  box-sizing: border-box;
  min-height: 24px;
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  padding: 2px var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
}
/* A field with no boat behind it is shown, not offered: the recessed tone says
   read-only rather than broken. */
.boat-input:disabled { color: var(--color-text-muted); cursor: default; }
/* A confirmed row says so at its end, in the hint register. The button beside it
   stays the same small bordered button in every state: colour marks nothing here. */
/* A row's button never wraps (IW-515): the track is 5em, sized for Confirm and
   Update, and Ask again broke onto two lines in it. A wider verb runs into the
   mark column beside it, which is empty on the one row that carries it. */
.boat-field .btn-small { justify-self: start; white-space: nowrap; }
.boat-confirmed {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* The panel's help lines (IW-359), in the .field-hint register: a size and a tone
   under the label they sit beneath, and capped at the prose measure, because the
   pane runs to about 140 characters and a hint that wide reads as body text. They
   were label sized and DARKER than the label, which inverted the hierarchy. */
.media-line {
  margin-top: var(--space-xs);
  max-width: var(--measure-prose);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
  /* The line is a disclosure since IW-368, and the brief-section details rule
     near the top of this file would give it a box and a bottom margin. It is a
     line, not a section, so neither. */
  border: 0;
  margin-bottom: 0;
}
/* The line as the summary of its own fold (IW-368). It keeps the size, tone and
   measure above, and the product's small triangle marker from the details rules,
   so it reads as a sentence that opens rather than as a section heading: the
   brief-section register (uppercase, tracked, bold, on the page background, in a
   padded box) is dropped here, declaration by declaration. The marker follows
   this fold's own state straight from those rules: they read the summary's own
   details since IW-416, so the per-fold marker override IW-368 needed (the
   panel around these folds is open, and the old descendant rule drew the panel's
   open marker on a closed line) is gone. */
.media-line > summary {
  display: inline-block;
  padding: 0;
  background: none;
  border-radius: 0;
  font-size: var(--font-size-xs);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--color-text-muted);
}
.media-line > summary:hover { color: var(--color-text-primary); }
/* The reasoning, once opened: the same size, one tone darker than the line it
   sits under, because it is the thing the operator asked to read. A tooltip on
   the control carries the same sentence for a mouse; this is the copy that
   touch and the keyboard can reach. */
.boat-why {
  padding: var(--space-xs) 0 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
/* The customer's own words naming a different boat than the field does
   (IW-318). Indented to the input's column so it reads as a remark about the
   value above it rather than a second value. */
/* NOT in the hint register above: a note can be a live disagreement, a confirmation
   a Freshdesk edit now overrides or a write that never landed, so it keeps the
   label's size. It shares only the measure. */
.boat-note {
  margin-top: var(--space-xs);
  margin-left: calc(10em + var(--space-sm));
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* The operator's System Type differs from the extractor's (IW-94), on the
   brief card: a live disagreement rather than a note. The panel says it under
   its sentence since IW-886. */
.brief-product-note {
  margin-top: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-status-waiting);
}
.brief-product-note { margin-bottom: var(--space-xs); }

/* The requester's partner row under the sentence (IW-517); the brief card's
   reading disagreement shares the product's live-warning colour (IW-99 U3). */
.reading-block { margin-bottom: var(--space-sm); }
.brief-reading-note {
  margin-top: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-status-waiting);
}
.brief-reading-note { margin-bottom: var(--space-xs); }
/* What confirming Requester type changes beyond this ticket (IW-376), said
   before the click. A standing fact about the control, not a state or a
   warning, so it takes the help lines' register: their size, tone and measure. */
.reading-contact-note {
  margin-top: var(--space-xs);
  max-width: var(--measure-prose);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* Media to read (IW-515): the remembered media choice and Ask again, on the
   panel's one row. The box is read-only and takes the disabled tone the row
   already uses for a value that is shown rather than offered; the button is the
   small bordered one every row has. Nothing new is drawn: the row is the fourth
   block in the same grid, and its help line rides the shared rule above. */
.media-block { margin-bottom: var(--space-sm); }

/* ── About this boat: the sentence (IW-886) ──────────────────────────────────
   The boat, its system and the two reads of the ticket as one paragraph of
   fact, with a slot where each value lives. The type sits a step above the
   pane's body so the paragraph reads as prose, at a measure short enough to
   take in at a glance. A slot is marked only by a hairline under it, dotted
   while it is empty, and gets its full ring when it has focus. A read, which
   Integra took from the ticket and nobody has kept yet, is the one value in
   the waiting tone; the accent is kept for Keep all and Undo, the two things
   a person presses here. Two classes deep throughout, because the form default
   for input[type=text] outranks a bare class. */
.atb-sentence-block { margin-bottom: var(--space-sm); }
.atb-sentence-block .atb-sentence {
  margin: var(--space-xs) 0;
  max-width: 40rem;
  font-size: 16px;
  line-height: 1.9;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
.atb-sentence .atb-slot {
  box-sizing: content-box;
  max-width: 100%;
  margin: 0;
  /* Narrow at the sides, so a comma or full stop after a slot sits where
     the paragraph's own type would put it. */
  padding: 1px 2px;
  border: 0;
  border-bottom: 1px solid var(--color-border-subtle);
  border-radius: 0;
  background: none;
  font: inherit;
  line-height: 1.4;
  color: var(--color-text-primary);
  white-space: nowrap;
  vertical-align: baseline;
}
/* A text slot is as wide as what it holds; chat.js measures it where the
   browser cannot. */
.atb-sentence input.atb-slot { field-sizing: content; width: auto; min-width: 2ch; }
.atb-sentence input.atb-slot::placeholder { color: var(--color-text-muted); opacity: 1; }
.atb-sentence .atb-slot:hover { background: var(--color-bg-hover); }
.atb-sentence .atb-slot:focus,
.atb-sentence .atb-slot:focus-within {
  outline: none;
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  box-shadow: 0 0 0 1px var(--color-accent);
}
.atb-sentence .atb-slot.is-empty { border-bottom-style: dotted; color: var(--color-text-muted); }
.atb-sentence .atb-slot.is-read {
  color: var(--color-status-waiting);
  background: var(--color-status-waiting-bg);
  border-bottom-color: var(--color-status-waiting);
}
.atb-sentence .atb-slot.is-mono { font-family: var(--font-mono); font-size: 0.875em; }
/* Words nothing can be typed into here (no boat to keep a place on, say) read
   as the paragraph's own text, with no line under them to promise otherwise. */
.atb-sentence .atb-slot.is-fixed { border-bottom-color: transparent; }
.atb-sentence .atb-slot.is-fixed:hover { background: none; }
/* A choice is its words with the list's own select laid over them, unseen, so
   a click opens Freshdesk's list and the paragraph keeps its type. */
.atb-sentence .atb-slot.is-select { position: relative; display: inline-block; cursor: pointer; }
.atb-sentence .atb-select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  font: inherit;
}
/* The strip of reads (IW-887): every value Integra read from the ticket,
   gathered under the paragraph and settled in one press. It wears the same
   waiting ground as the read slots it stands for, at the paragraph's own
   measure so its buttons stay beside its words. The count is the one part in
   the waiting tone; Keep all takes the accent, as the act a person came for,
   and Not these stays a quiet underlined word. On a phone the buttons drop
   under the words. */
.atb-sentence-block .atb-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  max-width: 40rem;
  margin: var(--space-xs) 0 var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--color-status-waiting-bg);
}
.atb-sentence-block .atb-strip-text {
  flex: 1 1 16rem;
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-primary);
  /* Keeps "Keep them?" from breaking onto a line of its own at phone width. */
  text-wrap: pretty;
}
.atb-sentence-block .atb-strip-count { color: var(--color-status-waiting); font-weight: 600; }
.atb-sentence-block .atb-strip-acts { display: inline-flex; align-items: center; gap: var(--space-xs); }
.atb-sentence-block .atb-keep-all {
  padding: 3px var(--space-sm);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-accent);
  font: inherit;
  font-size: var(--font-size-xs);
  font-weight: 500;
  line-height: 1.4;
  cursor: pointer;
}
.atb-sentence-block .atb-keep-all:hover { background: var(--color-accent); color: var(--color-text-on-accent); }
.atb-sentence-block .atb-not-these {
  padding: 3px 4px;
  border: 0;
  background: none;
  color: var(--color-text-secondary);
  font: inherit;
  font-size: var(--font-size-xs);
  line-height: 1.4;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.atb-sentence-block .atb-not-these:hover { color: var(--color-text-primary); }
.atb-sentence-block .atb-keep-all:disabled,
.atb-sentence-block .atb-not-these:disabled { opacity: 0.5; cursor: default; }
/* Under the paragraph, in the help lines' register: where the settled values
   came from, what stays on screen, and the quiet line a save leaves. */
.atb-sentence-block .atb-sources,
.atb-sentence-block .atb-help,
.atb-sentence-block .atb-saved {
  margin: var(--space-xs) 0 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
}
.atb-sentence-block .atb-saved { color: var(--color-text-secondary); }
/* After Keep all or Not these the strip is gone, and focus lands on the lines
   that say what happened when no Undo is left to take it (IW-887). */
.atb-sentence-block .atb-saved-lines { max-width: var(--measure-prose); }
.atb-sentence-block .atb-saved-lines:focus { outline: none; }
.atb-sentence-block .atb-saved-lines:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
.atb-sentence-block .atb-undo {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.atb-sentence-block .atb-undo:disabled { color: var(--color-text-muted); cursor: default; }

/* Ticket-open quick-look: condensed customer email with the verbatim original one click away. */
.inbound-card { border-left: 3px solid var(--color-accent); }
.inbound-condensed { margin-top: var(--space-xs); }
.inbound-original { margin-top: var(--space-sm); }
.inbound-original summary { cursor: pointer; color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.inbound-original-body {
  margin-top: var(--space-xs);
  padding: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-secondary);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* The team's newest entry, when it is newer than the customer's message (IW-557). It
   leads the card set as a quoted message, a rule down its left edge, held to eight lines
   so a long reply with its quoted history never pushes the customer's message off
   screen. No vertical padding, so the cap (8 lines at 1.5) falls on a line boundary
   rather than through one. The badge is the one coloured thing: it says whether the
   customer received the entry. The customer's message beneath takes a quieter
   subtitle, set off by a hairline. */
.inbound-latest-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-sm); }
.inbound-latest-body {
  max-height: 12em;
  overflow-y: auto;
  padding: 0 var(--space-sm);
  border-left: 3px solid var(--color-border-subtle);
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* A team member's own opening email (IW-795) is headed with the rail's Integrel mark
   rather than the word Customer; the mark sits a step before the heading's words. */
.inbound-card .staff-mark { margin-right: var(--space-xs); vertical-align: 1px; }
.inbound-customer-title {
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.rma-row { display: flex; align-items: center; gap: var(--space-sm); padding: 2px 0; font-size: var(--font-size-sm); }
.rma-key { color: var(--color-text-muted); min-width: 11em; }
/* A multi-line field (a quote's item list) keeps its rows, so the key sits at the
   top of the block rather than centred against a tall box. */
.rma-row:has(.rma-input-multiline) { align-items: flex-start; }
.rma-input-multiline { resize: vertical; line-height: 1.4; }
.rma-input {
  flex: 1 1 auto;
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  padding: 2px var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
}

/* The failed part's serial number on the RMA and quote previews (IW-653). The row keeps
   the form's two columns: its key on the left, and on the right the box with the boat's
   recorded serials under it. Each recorded serial is a press in the serial number card's
   own look (.device-type-option), because it is the same kind of choice about the same
   thing: the serial in the mono weight that card gives it, then what it is printed on in
   the secondary tone. They stack one to a line, since a serial with its type, ID and side
   is a sentence and not a chip. An empty box says what the card will say, in the warning
   tone and in words: the operator is about to approve a card that owes something. */
.rma-serial-row { align-items: flex-start; flex-wrap: wrap; margin-top: var(--space-xs); }
.rma-serial-row .rma-key { padding-top: 3px; }
/* The column takes the width the other rows' boxes take, whatever its presses hold: a
   basis of auto would size it from its longest press and leave the box short of the edge.
   Its basis is a sentence's worth of room, so on a phone, where the key leaves less than
   that, the column drops under its key and runs the card's width rather than setting the
   owed line five words to a line. */
.rma-serial { flex: 1 1 16em; min-width: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.rma-serial > .rma-input { flex: none; width: 100%; box-sizing: border-box; }
.rma-serial-owed { color: var(--color-status-warning); }
.rma-serial-lead { color: var(--color-text-muted); margin-top: var(--space-xs); }
.rma-serial-choices { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-xs); }
/* Two classes, so the press keeps the serial number card's look and still wraps: that
   card's own rule comes later in this sheet and holds its presses to one line, which a
   serial with its type, ID and side cannot always keep on a phone. */
.rma-serial-choices .rma-serial-choice {
  flex-wrap: wrap;
  gap: 0 var(--space-sm);
  max-width: 100%;
  padding-top: var(--space-xs);
  padding-bottom: var(--space-xs);
  white-space: normal;
  text-align: left;
}
.rma-serial-value {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
.rma-serial-detail { color: var(--color-text-secondary); font-weight: 400; }

/* Suggest Docs update (IW-480, IW-870). Unlike the flat Jira forms above, its fields
   carry prose, so each label sits over its box. The two short fields about the page
   share a line on a wide card; the rest run full width. The proposed text is set as
   the markdown source it is, in the code face on the code ground, because the IKB card
   carries it as a code block for the Docs team to paste. */
.docs-update-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md) var(--space-lg);
}
.docs-update-field { display: flex; flex-direction: column; gap: var(--space-xs); min-width: 0; }
.docs-update-field-wide { grid-column: 1 / -1; }
.docs-update-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.docs-update-hint { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.docs-update-input {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
}
textarea.docs-update-input { resize: vertical; }
.docs-update-source {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  line-height: 1.6;
  background: var(--color-bg-code);
}
.docs-update-note {
  margin: var(--space-md) 0 var(--space-xs);
  max-width: var(--measure-prose);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
}
/* Once filed the text is the record, so the boxes stop looking editable. */
.docs-update-filed .docs-update-input { border-color: transparent; background: var(--color-bg-page); }
.docs-update-filed .docs-update-source { background: var(--color-bg-code); }
/* Filed in IKB: the button gives way to the card's key, linked to it in Jira, in the
   resolved tone. The words say it was filed, so the colour carries nothing alone. */
.docs-update-done { color: var(--color-status-resolved); font-weight: 600; }
.docs-update-key { color: inherit; font-variant-numeric: tabular-nums; }

/* About this boat panel (IW-317): boat and system identification, plus the
   proposal review that moves into the chat under IW-315. */
.memory-panel {
  flex: none;
  margin: var(--space-sm) 0 0;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
}
.memory-head {
  cursor: pointer;
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  list-style: none; /* the chevron replaces the default disclosure marker */
}
.memory-head::-webkit-details-marker { display: none; }
.memory-head::marker { content: ""; }
/* The generic brief-section summary injects a text triangle (style.css
   "Brief sections"); the memory head's rotating chevron replaces it. The
   `.memory-panel` prefix carries the weight: `[open]` is an attribute selector
   and weighs as a class, so the open marker rule is (0,1,3) and a lone
   `.memory-head::before` at (0,1,1) loses to it, which drew a second triangle
   beside the chevron on the open panel in IW-416's visual check. Two classes
   (0,2,1) outrank it in both states, so one selector covers open and closed. */
.memory-panel .memory-head::before { content: none; }
.memory-head:hover { color: var(--color-text-primary); }
.memory-chev { width: 12px; height: 12px; flex: none; transition: transform 0.12s ease; }
@media (prefers-reduced-motion: reduce) { .memory-chev { transition: none; } }
.memory-panel[open] .memory-chev { transform: rotate(90deg); }
/* Bounded body (IW-53, IW-357). The 40vh cap this carried was written for the old
   open ended case memory panel, and it cropped a fixed form on an ordinary laptop.
   The panel is now nine rows and four help lines (the Media to read row landed
   with IW-515), about 23rem at rest. A seat with no Freshdesk field keys adds a
   note under each synced row and the blocks can each add a disagreement, which
   comes to about 34rem, so the cap clears that and the form shows whole. The
   viewport term is the IW-53 guard, kept for a short window: the body scrolls
   inside its frame before it shoves the conversation and composer off screen.
   The panel is closed until an operator opens it, which is why it may take more
   of the window than the old one did. */
.memory-body {
  padding: 0 var(--space-md) var(--space-md);
  max-height: min(36rem, 65vh);
  overflow-y: auto;
}
.proposal-card {
  margin-top: var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  padding: var(--space-sm);
  font-size: var(--font-size-sm);
}
.proposal-field {
  font-family: var(--font-mono);
  font-weight: 600;
  margin-right: var(--space-sm);
}
.proposal-value { overflow-wrap: anywhere; }
/* The shared builder's card reads claim, consequence, action (IW-360). .proposal-field
   and .proposal-value above are the registry card's, which still sets its diff on one
   line; the builder uses the three classes below. The registry card draws its type in
   the tag as well since IW-529, at its own weight (.rq-type, further down).
   The tag is the status line's quiet chip, and takes its look from the .status-tier
   rule it is grouped into further down: a classification, so it is the least emphatic
   thing on the card. Only its layout is set here. The claim takes the prose size, which
   makes it the largest type on the card and level with the agent's writing around it.
   It stops short of --font-size-claim because a proposed rule can run to several
   sentences. Its bottom padding, not a margin, carries the gap to whatever follows, so
   a figure card's picture gets the same step as another card's first line. */
.proposal-tag { width: fit-content; max-width: 100%; overflow-wrap: anywhere; }
.proposal-claim {
  margin-top: var(--space-xs);
  padding-bottom: 2px;
  font-size: var(--font-size-prose);
  font-weight: 500;
  line-height: 1.45;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
/* What approving does. The rule above it is the line between the case being made and
   the decision being asked for, so the sentence reads with the buttons and not as one
   more row of evidence. */
.proposal-effect {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
  color: var(--color-text-primary);
}
.proposal-effect ~ .proposal-actions { margin-top: var(--space-sm); }
/* Who raised the ticket, on a New boat card (IW-516). It opens the decision zone
   the way the effect line does, under the same rule, because it is the decision:
   choosing rewrites the effect line under it. The choices are the Review card's
   people block in miniature (.rq-choice, further down), one row of radios that
   wraps, with the trade select on a row of its own under the partner choice. The
   same hand as the block it stands in for, so an operator meets one control
   whichever screen they settle the card on. */
.proposal-people {
  margin: var(--space-sm) 0 0;
  padding: var(--space-sm) 0 0;
  border: 0;
  border-top: 1px solid var(--color-border-subtle);
  min-width: 0;
}
.proposal-people-lead {
  padding: 0;
  margin: 0 0 var(--space-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.proposal-people-choices {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  min-width: 0;
}
.proposal-choice {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
  color: var(--color-text-primary);
}
.proposal-choice input { margin: 0; flex: none; }
.proposal-people-why + .proposal-people-choices { margin-top: var(--space-xs); }
.proposal-people-trade { margin-top: var(--space-xs); }
.proposal-people-trade[hidden],
.proposal-people-note[hidden] { display: none; }
/* The product's form default gives every select the width of its container; this
   one sizes to the three trades it offers, like the Review card's. */
.proposal-trade-select { width: auto; max-width: 100%; }
.proposal-people-why,
.proposal-people-note,
.proposal-people-more { color: var(--color-text-muted); margin-top: var(--space-xs); }
.proposal-trigger { color: var(--color-text-muted); margin-top: 2px; }
/* The ticket's device prompt (IW-567). It opens the decision zone under the same
   rule the people block does, because it is the decision. Devices sit in groups by
   class, a quiet head over each, and a row is the device on the left and its state
   on the right: three positions, Fitted now, Removed or replaced, Not on this boat
   (IW-588), with one muted line under the question saying what each records. The
   state control is the card's one distinct piece. Its chosen position is filled and
   underlined in the accent, the colour every choice an operator makes takes; the
   family colour stays on the chip alone (IW-529). The ID keeps the registry card's
   mono weight (.proposal-field) beside its class in words, so a four character ID
   reads as an ID. Removed or replaced dims the device and Not on this boat strikes it
   through, so what Confirm will do is visible on the row before the click. At the
   pane's tight step the state drops under the device and its three labels share the
   width, each free to wrap onto two lines rather than run off the card. */
.proposal-devices {
  margin: var(--space-sm) 0 0;
  padding: var(--space-sm) 0 0;
  border: 0;
  border-top: 1px solid var(--color-border-subtle);
  min-width: 0;
}
.device-group + .device-group { margin-top: var(--space-md); }
.device-group-head {
  margin-bottom: var(--space-xs);
  color: var(--color-text-secondary);
  font-weight: 600;
}
.device-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-md);
  padding: var(--space-xs) 0;
}
.device-row + .device-row { border-top: 1px dashed var(--color-border-subtle); }
.device-who { flex: 1 1 14rem; min-width: 0; }
.device-name { color: var(--color-text-primary); overflow-wrap: anywhere; }
.device-id { font-family: var(--font-mono); font-weight: 600; }
.device-detail { color: var(--color-text-muted); margin-top: 2px; overflow-wrap: anywhere; }
/* When, and which unit is the older one (IW-669). Under a row's own detail: the reason
   the row opens as it does, one step up from the detail voice because it is the card's
   own judgement; the claim, in the quote a typed serial's sentence has; and the photo's
   two times as a labelled pair, so a sent time is never read as a taken one. */
.device-reason { color: var(--color-text-secondary); margin-top: var(--space-xs); max-width: var(--measure-prose); overflow-wrap: anywhere; }
.device-who > .device-quote { margin-top: var(--space-xs); }
.device-claim { margin-top: var(--space-xs); }
.device-claim > .device-detail { margin-top: 0; }
.device-times {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0 var(--space-sm);
  margin: var(--space-xs) 0 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.device-times dt { color: var(--color-text-secondary); }
.device-times dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.device-unmarked-note { margin: var(--space-sm) 0 0; color: var(--color-text-secondary); }
.device-unmarked-note[hidden] { display: none; }
/* The question under a device new to a boat that holds a fitted device of its
   class with no ID yet (IW-689): is this that unit? It takes the row's full
   width under the name and the state toggle, set in from the row's edge by the
   rule the ID panel uses, so it reads as that row's and not the card's. The
   answers are whole sentences naming a unit by its serial number and side, so
   they stack as radios rather than sit in the state toggle's segmented control;
   the one chosen is set in the toggle's own weight. Which unit it replaced, asked
   only when there are several, steps in once more under the answer that asks it. */
.device-unit {
  flex: 1 1 100%;
  min-width: 0;
  margin: var(--space-xs) 0 0;
  padding: var(--space-xs) 0 var(--space-xs) var(--space-sm);
  border-left: 2px solid var(--color-border-subtle);
}
.device-unit[hidden] { display: none; }
.device-unit-lead { color: var(--color-text-secondary); max-width: var(--measure-prose); }
.device-unit-options,
.device-unit-which {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}
.device-unit-which { padding-left: var(--space-md); }
.device-unit-which[hidden] { display: none; }
.device-unit-which > .device-detail { margin-top: 0; }
.device-unit-option {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  max-width: var(--measure-prose);
  color: var(--color-text-primary);
  cursor: pointer;
  overflow-wrap: anywhere;
}
.device-unit-option input { flex: none; margin: 0; position: relative; top: 1px; }
.device-unit-option:has(input:checked) { font-weight: 600; }
.device-unit-option:has(input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.device-row[data-state="former"] .device-name { color: var(--color-text-secondary); }
.device-row[data-state="not_this_boat"] .device-name {
  color: var(--color-text-muted);
  text-decoration: line-through;
}
.device-state {
  display: inline-flex;
  flex: none;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.device-state-option {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--control-height);
  padding: 0 var(--space-sm);
  color: var(--color-text-secondary);
  cursor: pointer;
  white-space: nowrap;
}
.device-state-option + .device-state-option { border-left: 1px solid var(--color-border-subtle); }
/* The radio stays in the page for the keyboard and the screen reader; the
   position it belongs to is what shows it. */
.device-state-option input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}
.device-state-option:has(input:checked) {
  background: var(--color-bg-selected);
  color: var(--color-text-primary);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--color-accent);
}
.device-state-option:has(input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}
/* A serial a photo shows (IW-649), answered in the card wherever the card is. The photo
   sits on the left at a size a label can be made out at, the serial beside it in the
   mono weight, and under the serial the one thing an operator can tell from a photo:
   the type of device, each a press, apart rather than joined so the four wrap whole on a
   phone. A chosen type takes the device state control's chosen look, fill and accent
   underline, because it is the same kind of choice. Not on this boat is not a type, so
   it comes last with a dashed edge, and choosing it strikes the serial through
   the way a device row is struck. The device ID is optional and reads that way: a text
   link under the choice, shown once a type is chosen, opening the IDs the boat already
   has as small presses and a four character box. A photo pressed opens under its row,
   as wide as the card, and closes where it opened. */
.device-serial {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-sm) 0;
}
.device-serial + .device-serial { border-top: 1px dashed var(--color-border-subtle); }
/* Several photos of one serial stack in the photo's own column, so every serial on the
   card starts at the same edge however many photos it has. */
.device-thumbs { display: flex; flex: none; flex-direction: column; gap: var(--space-xs); width: 72px; }
.device-thumb {
  display: block;
  flex: none;
  width: 72px;
  height: 72px;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--color-text-muted);
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
  cursor: zoom-in;
}
.device-thumb[aria-expanded="true"] {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
  cursor: zoom-out;
}
.device-thumb:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.device-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.device-serial-body { flex: 1 1 11rem; min-width: 0; }
.device-serial-value {
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
/* A serial a message states (IW-654): its sentence is the evidence, quoted under the
   serial where the photo line sits. A rule down its left edge says it is someone else's
   words, the serial inside it takes the mono weight of the value above so the two can
   be matched at a glance while the words around it stay a step quieter, and where it is
   from follows in the detail voice. */
.device-quote {
  margin: var(--space-xs) 0 0;
  padding: 0 0 0 var(--space-sm);
  border-left: 2px solid var(--color-border-subtle);
  max-width: var(--measure-prose);
}
.device-quote-text {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}
.device-quote-serial {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--color-text-primary);
}
.device-quote .device-detail { margin-top: 0; }
/* Where a serial's sentence came from (IW-750): one line above the quote saying who
   wrote it, in which entry of which ticket, and when, the writer in the primary tone
   and the rest a step quieter, at the quote's size. It replaces the "From a message"
   line that sat under the quote, so the quote keeps its place under the serial. */
.device-quote-source {
  margin: var(--space-xs) 0 0;
  max-width: var(--measure-prose);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}
.device-quote-who { color: var(--color-text-primary); font-weight: 600; }
.device-quote-source + .device-quote { margin-top: 2px; }
/* A Jira card named in the line: a small object in the sentence, not an address. Its
   key in the mono weight the serial takes, its status after a dot in the status's own
   tone (resolved green once Jira says it is done, the detail voice before), on a
   raised fill with the subtle edge. It sits on the text's baseline so the words
   around it keep their place, and takes the accent ring the photo's thumbnail takes
   while its details are open. */
.device-jira {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
  margin: 0 2px;
  padding: 0 var(--space-sm) 0 6px;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-raised);
  color: var(--color-text-primary);
  font: inherit;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  vertical-align: baseline;
  cursor: pointer;
}
.device-jira:hover { background: var(--color-bg-hover); }
.device-jira:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.device-jira[aria-expanded="true"] {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
}
.device-jira-key { font-family: var(--font-mono); font-weight: 600; }
.device-jira-status {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: 600;
}
.device-jira-status::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}
.device-jira-done { color: var(--color-status-resolved); }
/* Read the whole note, and the quiet line saying the ticket holds no photo of the
   label, in one wrapping row under the quote. */
.device-evidence {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-sm);
}
.device-evidence-open { padding: 3px var(--space-md); font-size: var(--font-size-sm); }
.device-evidence-open[aria-expanded="true"] {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
}
.device-evidence-none { color: var(--color-text-muted); font-size: var(--font-size-xs); }
/* The card's details or the whole message, opened inside the card under the serial
   and above its choices, as the photo opens (mockup option A). The page's own ground
   set into the card with the accent rule down its left edge marks it as something
   opened from the line above, and it holds the reading measure so a long note wraps
   as prose. Its rows are a label column in the secondary tone beside the values; the
   serial inside it takes the resolved fill, so the eye finds the match. */
.device-unfold {
  margin-top: var(--space-sm);
  padding: var(--space-md);
  max-width: var(--measure-prose);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-sm);
  background: var(--color-bg-page);
  font-size: var(--font-size-sm);
}
.device-unfold[hidden] { display: none; }
.device-unfold-top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-xs) var(--space-sm);
}
.device-unfold-top .device-detail { margin-top: 0; }
.device-unfold-key { font-family: var(--font-mono); font-weight: 600; font-size: var(--font-size-md); }
.device-unfold-status {
  padding: 0 var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--color-bg-raised);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: 600;
}
.device-unfold-status-done {
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
}
.device-unfold-title { margin: 2px 0 var(--space-sm); font-weight: 600; font-size: var(--font-size-md); }
.device-unfold-rows {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px var(--space-md);
  margin: 0;
}
.device-unfold-rows dt { color: var(--color-text-secondary); }
.device-unfold-rows dd { margin: 0; overflow-wrap: anywhere; }
.device-unfold-hit {
  padding: 0 4px;
  border-radius: var(--radius-sm);
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
  font-family: var(--font-mono);
  font-weight: 600;
}
.device-unfold-note {
  margin: var(--space-sm) 0 0;
  line-height: 1.55;
  white-space: pre-line;
  overflow-wrap: anywhere;
}
.device-unfold-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.device-unfold-link,
.device-unfold-rows a { color: var(--color-text-primary); }
.device-unfold-link { font-size: var(--font-size-sm); }
.device-unfold-close { margin-top: var(--space-sm); padding: 2px var(--space-sm); font-size: var(--font-size-xs); }
/* What an RMA did to the boat's devices, under a serial a message ties to it. The RMA is
   a replacement in one device's life, so the device is the subject: its name leads, and
   its two units follow in the order they were fitted, joined by a rule down the left
   because they are a sequence. The failed unit is history, in the muted tone and a
   hollow mark (never struck through: on this card a strike means not on this boat). The
   unit this serial is takes the accent mark and says so. The RMA key is the link to its
   Jira card, in the mono weight a key takes; the key in the quote below is then plain
   text, so the card has one control for the RMA. */
.device-rma {
  margin: var(--space-sm) 0 var(--space-md);
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
}
.device-rma-device { color: var(--color-text-primary); font-weight: 600; font-size: var(--font-size-md); }
.device-rma-device .device-rma-id { font-family: var(--font-mono); font-weight: 600; }
.device-rma-units {
  list-style: none;
  margin: var(--space-xs) 0 0 5px;
  padding: 0 0 0 var(--space-lg);
  border-left: 2px solid var(--color-border-subtle);
}
.device-rma-unit { position: relative; display: flex; flex-wrap: wrap; gap: 0 var(--space-sm); align-items: baseline; padding: 2px 0; }
.device-rma-unit + .device-rma-unit { margin-top: var(--space-xs); }
.device-rma-unit::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-lg) - 6px);
  top: 0.55em;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--color-text-muted);
  background: var(--color-bg-surface);
}
.device-rma-this::before { border-color: var(--color-accent); background: var(--color-accent); }
.device-rma-serial { font-family: var(--font-mono); font-weight: 600; color: var(--color-text-primary); min-width: 4ch; }
.device-rma-former .device-rma-serial { color: var(--color-text-secondary); }
.device-rma-what { color: var(--color-text-secondary); }
.device-rma-unknown { color: var(--color-text-muted); }
.device-rma-you { color: var(--color-accent); font-weight: 600; }
.device-rma-key {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--color-text-primary);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.device-rma-hint { margin: var(--space-xs) 0 0; color: var(--color-text-secondary); font-size: var(--font-size-xs); }
.device-quote-key { font-family: var(--font-mono); font-weight: 600; color: var(--color-text-primary); }
/* The sentence a boat fact was read from (IW-655), on its card between the claim and
   the decision. The typed serial's quote above, at the card's level: the same rule down
   the left edge for someone else's words, the same secondary tone, and where it is from
   in the muted detail voice under it. It takes the size of the card it is on, small in
   the conversation and the body size on Learnings, and holds a reading measure, so a
   long sentence wraps as prose and never widens the card. Nothing is drawn for a fact
   with no sentence, which leaves the card exactly as it was. */
.proposal-quote {
  margin: var(--space-xs) 0 0;
  padding: 0 0 0 var(--space-sm);
  border-left: 2px solid var(--color-border-subtle);
  max-width: var(--measure-prose);
}
.proposal-quote-text {
  margin: 0;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.proposal-quote-from { color: var(--color-text-muted); }
/* On the Learnings card the ticket line follows the sentence, and takes the step the
   sentence took above itself, so the rule down the quote's edge ends clear of it. */
.proposal-quote + .rq-meta { margin-top: var(--space-xs); }
.rq-address-quote[hidden] { display: none; }
/* On a card that also has a photo, a serial with none starts where the others do, past
   the photo's column, so every serial on the card keeps one edge. */
.device-serials-pictured .device-serial-typed { padding-left: calc(72px + var(--space-md)); }
.device-serial[data-choice="not_this_boat"] .device-serial-value {
  color: var(--color-text-muted);
  text-decoration: line-through;
}
.device-type {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}
.device-type-option,
.device-side-option,
.device-id-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--control-height);
  padding: 0 var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-secondary);
  font: inherit;
  white-space: nowrap;
  cursor: pointer;
}
.device-type-option:hover,
.device-side-option:hover,
.device-id-chip:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
.device-type-option:focus-visible,
.device-side-option:focus-visible,
.device-id-chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.device-type-option[aria-pressed="true"],
.device-side-option[aria-pressed="true"],
.device-id-chip[aria-pressed="true"] {
  background: var(--color-bg-selected);
  color: var(--color-text-primary);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--color-accent);
}
.device-type-not { border-style: dashed; }
/* .btn-link is coloured for the inverse header; this sits on a card. */
.device-id-open {
  display: inline-block;
  margin-top: var(--space-xs);
  padding: 0;
  color: var(--color-text-secondary);
  opacity: 1;
  font-size: var(--font-size-sm);
  text-decoration: underline;
}
.device-id-open:hover,
.device-id-open[aria-expanded="true"] { color: var(--color-text-primary); }
.device-id-panel {
  margin-top: var(--space-xs);
  padding: var(--space-xs) 0 var(--space-xs) var(--space-sm);
  border-left: 2px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
}
.device-id-panel > .device-detail { margin-top: 0; max-width: 60ch; }
.device-id-known,
.device-id-typed {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-xs);
}
.device-id-known > .device-detail { margin-top: 0; }
.device-id-chip-note { color: var(--color-text-muted); font-weight: 400; }
.device-id-typed { color: var(--color-text-muted); }
.device-id-typed input { width: 7ch; font-family: var(--font-mono); text-transform: uppercase; }
.device-id-typed + .device-detail { margin-top: var(--space-xs); }
.device-photo { flex: 1 1 100%; min-width: 0; }
.device-photo-img {
  display: block;
  max-width: min(100%, 44rem);
  max-height: 70vh;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
}
.device-photo-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-xs);
}
.device-photo-foot .device-detail { margin-top: 0; }
/* A controller's side (IW-650): a second, smaller choice under the type, in the ID
   press's own form with a muted lead, shown only for a serial marked Controller. */
.device-side {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-xs);
  font-size: var(--font-size-sm);
}
.device-side > .device-detail { margin-top: 0; }
/* Where Confirm will put the serial (IW-682): one line under the row's choices, last
   because it is their outcome. It is a consequence and not a hint, so it takes the
   secondary tone, a step up from the muted detail voice around it, at the size of the
   choices it sums up, and holds a reading measure. A second device with no ID yet is the
   one outcome that adds a record nobody asked for by name, so that line alone takes the
   primary tone and the weight a chosen press takes. */
.device-attach {
  margin-top: var(--space-sm);
  max-width: 60ch;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}
.device-attach[data-to="new"] { color: var(--color-text-primary); font-weight: 600; }
/* A serial the boat already records on a device (IW-712): the same line in the same
   voice, saying where the serial already is, in place of the choices it no longer has.
   It sits a step closer to the serial because nothing stands between them. */
.device-on-file { margin-top: var(--space-xs); }
.device-attach[hidden],
.device-side[hidden],
.device-side-option[hidden],
.device-id-open[hidden],
.device-id-panel[hidden],
.device-id-known[hidden],
.device-photo[hidden] { display: none; }
/* What each state records, and the boat question's hint: one muted line in the
   people block's note voice, straight under the question it explains. */
.device-help { color: var(--color-text-muted); margin: 0 0 var(--space-sm); }
.device-lead { margin: 0 0 var(--space-xs); }
/* Which boat, on a card with none (IW-588). It is the first question, so it takes
   the legend, and the device choices wait under it, hidden, until a boat is chosen.
   The search is the registry's boat picker brought onto the card: an input with New
   boat beside it, then the matches as an inline list rather than a floating panel,
   because the card sits in a scrolling chat where a popover would clip. Each match is
   a whole-row button, the name in the claim's weight and the owner and what matched
   in muted text, the registry suggestion row's grammar. The chosen boat replaces the
   search with its name and a link to choose again, the candidate card's picked state.
   New boat, once pressed, takes the device state control's chosen look, fill and
   accent underline, because it is the same kind of choice. */
.device-boat { margin-bottom: var(--space-xs); }
.device-boat-find {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
}
.device-help[hidden],
.device-boat-find[hidden],
.device-boat-list[hidden],
.device-boat-note[hidden],
.device-boat-picked[hidden],
.device-choices[hidden] { display: none; }
.device-boat-input { flex: 1 1 14rem; min-width: 0; max-width: 28rem; }
.device-boat-new { flex: none; height: var(--control-height); }
.device-boat-new[aria-pressed="true"] {
  background: var(--color-bg-selected);
  color: var(--color-text-primary);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--color-accent);
}
.device-boat-list {
  display: flex;
  flex-direction: column;
  max-width: 28rem;
  max-height: 16rem;
  overflow-y: auto;
  margin-top: var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-raised);
}
.device-boat-option {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-sm);
  width: 100%;
  padding: var(--space-xs) var(--space-sm);
  border: 0;
  background: none;
  color: var(--color-text-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.device-boat-option + .device-boat-option { border-top: 1px solid var(--color-border-subtle); }
.device-boat-option:hover { background: var(--color-bg-hover); }
.device-boat-option:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.device-boat-name { font-weight: 600; overflow-wrap: anywhere; }
.device-boat-detail { color: var(--color-text-muted); font-size: var(--font-size-sm); overflow-wrap: anywhere; }
.device-boat-note { color: var(--color-text-muted); margin-top: var(--space-xs); }
.device-boat-picked {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
}
.device-boat-picked-name { font-weight: 600; color: var(--color-text-primary); overflow-wrap: anywhere; }
/* .btn-link is colored for the inverse header; this sits on a card. */
.device-boat-again { color: var(--color-accent); opacity: 1; padding: 0; }
.device-choices { margin-top: var(--space-sm); }
@container pane (max-width: 560px) {
  .device-state { width: 100%; }
  .device-state-option {
    flex: 1 1 0;
    justify-content: center;
    padding: var(--space-xs);
    white-space: normal;
    text-align: center;
    line-height: 1.25;
  }
  .device-boat-input { flex-basis: 100%; max-width: none; }
}
/* Up to four buttons on a technical note that resembles another (Merge, Keep both,
   Edit, Dismiss, IW-519). On a phone they wrap to a second row rather than folding
   each label onto three lines. */
.proposal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-top: var(--space-xs); }
.proposal-actions button { white-space: nowrap; }
/* A proposal card in the chat stream (IW-315, IW-362). It is a direct child of the
   assistant turn, like the artifact, approval and decision cards, so it is capped at
   their width. The cap is scoped to the chat because the Learnings page gives the same
   card its own size and measure (.learnings-page .proposal-card, below) and the registry
   page draws it inside a column that already sets one. Everything
   else it inherits from .proposal-card above, which is the point of one builder for
   both views.
   A ch is measured in the element's own type size, and the siblings measure theirs at
   the size the turn hands down. So in the chat the card's box keeps that size and the
   small type moves onto its rows, which makes the one token mean the one width.
   The row rule sits inside :where() so it weighs nothing (IW-360). It is a default for
   rows that say nothing about their size, and a row that does say, the claim or the
   tag, wins with its one class in the chat exactly as it does on the Learnings page.
   Written as a plain child selector it outranked every lone class, and the claim fell
   back to the small size in the chat and nowhere else. */
.msg-assistant > .proposal-card { font-size: inherit; max-width: var(--measure-wide); }
:where(.msg-assistant > .proposal-card) > * { font-size: var(--font-size-sm); }
/* The learning on file that a proposal contradicts (IW-680). It is the other half of
   the choice, so its wording takes the primary tone the claim has, where a supporting
   line is muted; who taught it and when leads it in the secondary tone. A step of space
   above sets it apart from the proposed learning's own lines. */
.proposal-onfile { margin-top: var(--space-sm); }
.proposal-onfile-from { color: var(--color-text-secondary); font-weight: 600; }
.proposal-onfile-text { color: var(--color-text-primary); overflow-wrap: anywhere; }
.proposal-status { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.proposal-source { color: var(--color-text-muted); margin-top: 2px; }
/* A settled card carries no controls and states what happened, so it drops the
   pending card's warning edge and reads as a record rather than a request. */
.proposal-settled { border-left-color: var(--color-border-subtle); }
.proposal-outcome {
  color: var(--color-text-muted);
  font-style: italic;
  margin-top: var(--space-xs);
}
/* The late serial offer (IW-653): a confirmed serial number card on a boat whose open RMA
   was filed without a serial offers to add it. The card above it is settled, a record, so
   the offer opens under a rule as its own small decision: one sentence naming the RMA in
   the primary tone, then the presses. Once pressed either way it reads as a record too,
   in the settled card's own outcome line. */
.serial-offer {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.serial-offer-line { color: var(--color-text-primary); }
.serial-offer-actions { margin-top: var(--space-sm); }
.serial-offer-open { color: var(--color-accent); font-size: var(--font-size-sm); }
.serial-offer-settled .serial-offer-line { color: var(--color-text-secondary); }
.proposal-figure-image {
  display: block;
  max-width: 100%;
  margin-top: var(--space-xs);
  border-radius: var(--radius-sm);
}
.figure-confirm {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
  color: var(--color-text-secondary);
}

/* The grouped card (IW-528): several registry proposals raised together for one boat,
   drawn in the chat as one card with a row each. It is a .proposal-card and keeps that
   card's frame, stripe, chip and decision zone; what is new is the list between them.
   The card exists to take away repeated furniture, so the rows carry as little as a
   row can: a mark, the headline, and two small buttons. The headline keeps the single
   card's claim size, so the values are the largest type on the card and lead it. The
   buttons are the outline button at its small size, and Dismiss steps down again to
   the subtle border, so a column of four pairs stays quieter than the four values
   beside it. The one filled button on the card is Confirm all.
   A hairline sits above every row and the decision zone's own rule closes the list, so
   the rows read as one ruled list and not as four small cards.
   The row is a grid, not a wrapping flex row, so that every row breaks the same way:
   on a wide pane the buttons hold the right edge, level with the headline's first
   line, and at the pane's tight step every row puts them under its headline, indented
   to it, whether its headline is long or short. */
.proposal-group-origin { margin-top: var(--space-xs); }
.proposal-rows { margin-top: var(--space-sm); }
.proposal-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-xs) var(--space-sm);
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--color-border-subtle);
}
/* The list marker, and the single card's coloured chip in miniature: a hollow box in
   the family's colour (the rules are with the chip's, further down), so rows of
   different kinds read apart the way four cards did. Outline only, like the chip. A
   row with no family keeps the muted outline. An empty box sits on the baseline, which
   puts it level with the lowercase letters of the headline beside it. */
.proposal-row-mark {
  width: 7px;
  height: 7px;
  border: 1px solid currentColor;
  border-radius: 2px;
  color: var(--color-text-muted);
}
.proposal-row-claim { min-width: 0; }
.proposal-row-headline {
  font-size: var(--font-size-prose);
  font-weight: 500;
  line-height: 1.45;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
/* What a row says beyond its headline: where it came from when the group's one origin
   line cannot speak for it, its own sentence when Confirm does more than the group's
   sentence says, and the error a failed Confirm left. Evidence weight, like the single
   card's supporting lines. */
.proposal-row-note { color: var(--color-text-muted); margin-top: 2px; overflow-wrap: anywhere; }
.proposal-row-notes:empty,
.proposal-row-status:empty { display: none; }
.proposal-row-actions { display: flex; gap: var(--space-xs); }
.proposal-row-actions button { white-space: nowrap; padding: 3px var(--space-md); }
.proposal-row-actions .proposal-dismiss:not(:disabled) {
  border-color: var(--color-border-subtle);
  color: var(--color-text-secondary);
}
/* A refusal belongs to its row and can run long, so it takes the row's full width under
   the headline. */
.proposal-row-status { grid-column: 2 / -1; }
/* A settled row is a record: the headline steps back to regular weight and the
   secondary tone, and what happened stands where the buttons were, in the words and
   the hand a settled single card uses. The pending rows are then the ones that lead. */
.proposal-row.proposal-settled .proposal-row-headline {
  font-weight: 400;
  color: var(--color-text-secondary);
}
.proposal-row-state { color: var(--color-text-muted); font-style: italic; }
/* The decision zone: the sentence for what Confirm does, said once for every row, with
   Confirm all beside it on the buttons' edge. The rule above it is the one the single
   card draws above its own sentence, and here it also closes the list, so the zone
   draws it and the sentence inside gives up its own. The sentence keeps a reading
   measure where the card is wide, and the button drops under it where it is not. It
   holds the right edge at every width: on a phone the rows' own buttons and a single
   card's Confirm sit on the left, and the button that settles every row must never
   stand where a thumb was already going for one of those. */
.proposal-group-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.proposal-group-foot .proposal-effect {
  flex: 1 1 20rem;
  min-width: 0;
  max-width: var(--measure-prose);
  margin: 0;
  padding: 0;
  border: 0;
}
.proposal-group-foot .proposal-confirm-all { flex: none; margin-left: auto; white-space: nowrap; }
/* On a phone the buttons are pressed with a thumb, and under the headline they have the
   room to be taller without crowding the values. */
@container pane (max-width: 560px) {
  .proposal-row-actions,
  .proposal-row-state { grid-column: 2 / -1; }
  .proposal-row-actions { gap: var(--space-sm); margin-top: 2px; }
  .proposal-row-actions button { padding: 6px var(--space-lg); }
}

/* Link ticket suggestions (IW-926): the ticket a row offers, under its headline. The
   subject is what tells one row from the next, so it takes the secondary tone a quote
   takes, a step above the evidence voice; why the brief asked follows in the muted
   evidence voice the row's notes use. No rule or box of its own: the row already has
   one, and the two lines read as the headline's own small print. A reading measure
   keeps a long subject from running under the buttons' column on a wide pane. */
.proposal-about { margin-top: 2px; max-width: var(--measure-prose); }
.proposal-about-ticket { color: var(--color-text-secondary); overflow-wrap: anywhere; }
.proposal-about-why { color: var(--color-text-muted); overflow-wrap: anywhere; }
.proposal-card > .proposal-about { margin-top: var(--space-xs); }

/* Learnings page (IW-548): everything waiting on a person, replacing the
   team queue (IW-315) and the registry's queue view (IW-249). The cards are
   the .proposal-card the chat draws and the registry card the boat page draws,
   so the page frame lives here and one rule for how a card is read on it.
   This is the page where an operator reads a dozen unfamiliar claims in a row, so
   the card's rows step up from the small size to the body size and the card takes
   its own measure (IW-361). At the small size the 46rem column set 110 characters a
   line and more. The size goes on the card's box, so the ch is measured in the type
   the rows are read in, and the claim and the tag still win with their own class as
   they do everywhere else. The chat does the reverse, box at the turn's size and small
   type on the rows, because there the card has to match the width of its siblings;
   here it has none. The status text stays at the small size with the buttons it sits
   beside, which are controls and keep the control size across the app. The intro takes
   the same size and the same box width, so it ends where the cards end.
   60ch and not --measure-prose: a ch is the width of the zero, 0.6em in Plex Sans,
   and running English in it averages about 0.42em a character. Measured in Chrome
   with the vendored face, 68ch at this size sets 87 to 90 characters a line; 60ch
   sets the rows at 76 to 80 and the larger claim at 66 to 71. */
.learnings-page { max-width: 46rem; }
.learnings-page .proposal-card,
.learnings-page .learnings-lede {
  font-size: var(--font-size-md);
  max-width: 60ch;
}
.learnings-page .rq-pii-note { max-width: 60ch; }
/* One queue fills the page (IW-651). The page title names it, with its count
   beside it as data; one sentence says what settling an item here does, read
   before the first card; then who can see it, and the queue. The sentence and
   the queue's box take the cards' size and measure, so everything ends where
   the cards end rather than at the page column. */
.learnings-section {
  font-size: var(--font-size-md);
  max-width: 60ch;
  margin-top: var(--space-lg);
}
/* The Freshdesk history's ledger sets its own column and measure
   (.history-review), a little wider for the row's box. */
.learnings-page-ledger { max-width: 50rem; }
.learnings-page-ledger .learnings-lede,
.learnings-page-ledger .rq-pii-note { max-width: 62ch; }
.learnings-section[hidden] { display: none; }
.learnings-count {
  margin-left: var(--space-xs);
  font-weight: 400;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.learnings-count[hidden] { display: none; }
.learnings-lede {
  color: var(--color-text-secondary);
  margin: var(--space-xs) 0 var(--space-md);
}
.learnings-note { color: var(--color-text-muted); }
/* A queue with nothing in it. It stands where the cards would, under a hairline
   and a step of space, so it reads as what the queue holds and not as a third
   line of the introduction above it; in the secondary tone, since on an empty
   queue it is the page's whole answer. */
.learnings-empty,
.learnings-page-ledger .hr-empty {
  font-size: var(--font-size-md);
  max-width: 60ch;
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
  color: var(--color-text-secondary);
}
.learnings-page-ledger .hr-empty { max-width: 62ch; }
.learnings-empty[hidden] { display: none; }
.learnings-error { color: var(--color-status-fault); }
/* Learnings links (IW-819). Every link on a Learnings queue that no rule of its own
 * colours, the card's "Raised on ticket N" first among them, reads in the quiet
 * register IW-800 gave the Boats pages' links: the text colour with a muted underline,
 * so none is left in the browser's blue, or its lilac in the dark theme. :where()
 * keeps it at zero specificity, so a link styled on purpose (a ledger row's ticket)
 * keeps its own look. The Boats rule names the two Boats
 * queues the Learnings template draws; this one is the Learnings page's own. */
:where(.learnings-page) a {
  color: var(--color-text-primary);
  text-decoration: underline;
  text-decoration-color: var(--color-text-muted);
  text-underline-offset: 2px;
}
/* Live from chat (IW-680): learnings an operator stated, live with no approval,
   listed under the team queue to be read back. A second tier of the page, so it
   opens a full step below the queue under a heading one size down from the page
   title, and it is a ledger and not cards: a card here asks for a decision and
   says so with its warning edge, and nothing in this list waits on anyone.
   A row sets up one comparison. What Integra recorded is the claim, at the
   card's claim size since it is the same thing a card would have proposed. The
   sentence the operator typed follows under the quote's own rule, the mark the
   product already draws beside somebody's words, with who approved it and on
   which ticket as its "from" line. The day sits against the right edge of the
   first line, where the eye runs down a column of them. Edit and Retire are
   secondary buttons at the row's foot: reading is the job, undoing the
   exception. The list takes the cards' measure, so it ends where they end. */
.live-learnings {
  font-size: var(--font-size-md);
  max-width: 60ch;
  margin-top: calc(var(--space-xl) * 2);
}
.live-learnings-head {
  margin: 0;
  font-size: var(--font-size-title);
  font-weight: 600;
  line-height: 1.3;
}
.live-learnings .learnings-lede { margin-bottom: var(--space-sm); }
.live-learnings .rq-pii-note { margin-bottom: var(--space-md); }
.live-learnings-empty {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
  color: var(--color-text-secondary);
}
.ll-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--color-border-subtle); }
.ll-row {
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--color-border-subtle);
}
.ll-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
}
.ll-when {
  flex: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ll-claim { margin: var(--space-xs) 0 0; }
.ll-line {
  margin: 2px 0 0;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.ll-line[hidden],
.ll-edit[hidden],
.ll-confirm[hidden],
.ll-actions[hidden],
.ll-claim-block[hidden] { display: none; }
/* The operator's sentence takes a full step from the learning above it, so the
   two read as two voices and not as one more line of the note. */
.ll-said { margin-top: var(--space-sm); }
.ll-said .proposal-quote-from { font-size: var(--font-size-sm); margin-top: 2px; }
.ll-ticket { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.ll-ticket:hover { color: var(--color-accent); }
.ll-edited { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.ll-actions { margin-top: var(--space-sm); }
.ll-edit { margin-top: var(--space-sm); max-width: none; }
.ll-edit .proposal-wording-input { width: 100%; box-sizing: border-box; }
/* Retire's second press. The sentence says what the press does in the primary
   tone, as the effect line on a card does, since it is the whole decision. */
.ll-confirm { margin-top: var(--space-sm); }
.ll-confirm-line { margin: 0; color: var(--color-text-primary); }
.ll-status {
  margin: var(--space-xs) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}
.ll-status:empty { display: none; }
.ll-refused { color: var(--color-status-fault); font-weight: 400; }
/* A retired row is a record: what it said steps down to the muted tone and its
   outcome reads in the settled card's own voice. */
.ll-retired .ll-claim,
.ll-retired .ll-line,
.ll-retired .proposal-quote-text { color: var(--color-text-muted); }
.ll-retired .ll-claim { font-weight: 400; }

/* How far a learning has got toward the repo (IW-686, _fold_state.html): the
   last line of a row's record, at the size of the "from" line above it, a full
   step below so it reads as its own fact. It is a sentence and not a chip: a
   chip in the row's head read as a control, and nothing here is pressed.
   The mark before the words is a passage with two ends, this instance on the
   left and the repo on the right, drawn in hairlines and the status tones the
   product already has. The state is how far the line has got: dotted and
   faint for a learning that is only live here, dashed in the waiting tone,
   solid to a filled end once it is in the repo, and cut short in the fault
   tone when the fold is stuck. Read down a list, the marks show at a glance
   which rows have arrived. The words carry the meaning on their own. */
.ll-fold {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  font-size: var(--font-size-sm);
}
.ll-fold-words { min-width: 0; }
.ll-fold-label { color: var(--color-text-primary); font-weight: 500; }
.ll-fold-live .ll-fold-label { color: var(--color-text-secondary); font-weight: 400; }
.ll-fold-stuck .ll-fold-label { color: var(--color-status-fault); font-weight: 600; }
.ll-fold-track {
  flex: none;
  display: inline-flex;
  align-items: center;
  width: 44px;
  transform: translateY(-2px);
  color: var(--color-text-muted);
}
.ll-fold-track i { display: block; box-sizing: border-box; }
.ll-fold-here,
.ll-fold-there {
  width: 8px;
  height: 8px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}
.ll-fold-here { background: currentColor; }
.ll-fold-way { flex: 1; height: 0; border-top: 1.5px dotted currentColor; opacity: 0.55; }
.ll-fold-waiting .ll-fold-track,
.ll-fold-leaving .ll-fold-track { color: var(--color-status-waiting); }
.ll-fold-waiting .ll-fold-way,
.ll-fold-leaving .ll-fold-way { border-top-style: dashed; opacity: 1; }
.ll-fold-repo .ll-fold-track { color: var(--color-status-resolved); }
.ll-fold-repo .ll-fold-way { border-top-style: solid; opacity: 1; }
.ll-fold-repo .ll-fold-there { background: currentColor; }
.ll-fold-stuck .ll-fold-track { color: var(--color-status-fault); }
.ll-fold-stuck .ll-fold-way { border-top-style: solid; opacity: 1; margin-right: 10px; }
/* The way to the fold's controls, for an admin, on a stuck row only. */
.ll-fold-admin { display: none; white-space: nowrap; }
.ll-fold-stuck .ll-fold-admin { display: inline; }
@media (forced-colors: active) {
  .ll-fold-track { forced-color-adjust: none; color: CanvasText; }
}

/* Waiting for the next fold (IW-686), the queue the spine's Fold badge opens.
   The Live from chat ledger standing as a queue of its own, as Support
   strategies does. A stuck fold is said once above the rows, in the fault
   tone, with the hairline the list would have opened with. */
.lw-fold { margin-top: var(--space-md); }
.lw-fold .ll-fold { margin-top: var(--space-xs); }
.lw-stuck {
  margin: 0 0 var(--space-md);
  color: var(--color-status-fault);
  overflow-wrap: anywhere;
}
/* A figure waiting for its image check (IW-695). The one row on the queue that
   asks a person to look, so the image is the row: laid on the surface tone as
   a print on a light table, as wide as the column and never taller than a
   screen's comfortable look, whole and never cropped, since an edge is where a
   name or a serial hides. The caption sits under it as a figure's caption
   does. The sentence the person agrees to and the button follow, in the
   primary tone a Retire's second press uses, since the press is the whole
   decision. Once settled the image steps back and the controls go. */
.lw-check-figure { margin: var(--space-sm) 0 0; }
.lw-check-frame {
  display: flex;
  justify-content: center;
  padding: var(--space-sm);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}
.lw-check-frame:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.lw-check-frame img {
  display: block;
  max-width: 100%;
  max-height: min(26rem, 60vh);
  object-fit: contain;
}
.lw-check-caption {
  margin-top: var(--space-xs);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}
.lw-check-decide { margin-top: var(--space-sm); }
.lw-check-decide[hidden] { display: none; }
.lw-check-full { font-size: var(--font-size-sm); }
.lw-check-done .lw-check-frame { opacity: 0.6; }
@media (prefers-reduced-motion: no-preference) {
  .lw-check-done .lw-check-frame { transition: opacity 200ms ease-out; }
}

/* Support strategies (IW-679), the queue of every rule of judgement in force.
   It is the Live from chat ledger standing as a queue of its own, so it starts
   where a queue's items start and not a section's step below them. Record a
   strategy sits above the rows as one secondary button: adding is the
   exception on a list whose job is to be read. Open, it is the row's own Edit
   boxes with a line saying what a strategy is, and it closes the list off with
   the step a row takes from the one above it. */
.ls-strategies { margin-top: var(--space-md); }
/* Every row here is a strategy, so a row carries no type tag, and its day sits
   on the line of its name, against the right edge where the column of days
   runs. */
.ls-strategies .ll-head { float: right; margin-left: var(--space-md); }
.ls-strategies .ll-claim { margin-top: 0; }
.ls-record { margin-bottom: var(--space-md); }
.ls-record .ll-edit { margin-top: 0; }
.ls-record [data-record-open][hidden],
.ls-record [data-record-form][hidden] { display: none; }
/* The crowded list's warning, in the caution voice the page's notices use. */
.ls-crowded {
  margin: 0 0 var(--space-md);
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
}
.ls-record-note {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* Playbooks (IW-678), the queue of every procedure in force. A playbook is a
   procedure, and its row is set so it reads as one. The name leads at the
   claim's weight. Under it runs ONE gutter, the width of the longest label,
   and everything in the row hangs from it: the labels Trigger, Runs and Reads
   sit in it, and so do the step numbers, set against its right edge. So the
   steps line up under the trigger they answer, the numbers read as a column,
   and nothing in the row needs a box or a rule to hold it together. The
   numbers are the one thing here that is drawn with intent: they are tabular,
   in the secondary tone, and the steps beside them are the only lines in the
   row at the primary tone besides the trigger, because the trigger and the
   steps are what an operator came to read. Where it runs and what it reads
   are facts about the procedure, a step quieter. No fill, no colour of its
   own, no token: the ledger's hairline between rows is the only rule. */
.lp-playbooks { margin-top: var(--space-md); }
.lp-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
}
.lp-head .lp-name { margin: 0; font-size: inherit; overflow-wrap: anywhere; }
.lp-facts { margin: var(--space-xs) 0 0; }
/* The gutter: one width for the labels and the step numbers, written once for
   both so the two columns cannot drift apart. */
.lp-fact,
.lp-steps li {
  display: grid;
  grid-template-columns: 4.5em minmax(0, 1fr);
}
.lp-fact { margin-top: 2px; }
.lp-fact dt {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  /* The smaller label sits on the baseline of the value beside it. */
  line-height: calc(1.5 * var(--font-size-md));
}
.lp-fact dd {
  margin: 0;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.lp-fact .lp-trigger { color: var(--color-text-primary); }
.lp-steps {
  list-style: none;
  margin: var(--space-sm) 0;
  padding: 0;
  counter-reset: lp-step;
}
.lp-steps li {
  margin-top: var(--space-xs);
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
  counter-increment: lp-step;
}
.lp-steps li:first-child { margin-top: 0; }
.lp-steps li::before {
  content: counter(lp-step);
  /* Against the gutter's right edge, a space clear of the step, so one and
     two digit numbers end on the same line. */
  padding-right: var(--space-sm);
  text-align: right;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.lp-page { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.lp-page:hover { color: var(--color-accent); }
.lp-page:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.lp-page-when { color: var(--color-text-muted); white-space: nowrap; }
.lp-from { margin-top: var(--space-sm); }
/* The form's own pieces: a hint under a box in the muted tone at the label's
   size and regular weight, the choice drawn as the boxes are, and the command
   box put away while the playbook runs on every ticket. */
.lp-hint { font-weight: 400; color: var(--color-text-muted); }
.lp-choice { width: 100%; box-sizing: border-box; resize: none; }
.ll-edit .lp-field input.proposal-wording-input { width: 100%; box-sizing: border-box; }
.lp-field[hidden] { display: none; }
.ll-retired .lp-steps li,
.ll-retired .lp-fact .lp-trigger,
.ll-retired .lp-fact dd { color: var(--color-text-muted); }
.ll-retired .lp-name { font-weight: 400; }
/* On the pane's tight step the gutter gives the steps back some of its width. */
@container pane (max-width: 560px) {
  .lp-fact,
  .lp-steps li { grid-template-columns: 3.75em minmax(0, 1fr); }
}

/* Boats from the Freshdesk history (IW-564). A ledger rather than cards: the job
   is to read down hundreds of names and tick the real boats, so each name is one
   hairline row, the box on the left where the eye runs down the column, the name
   in the registry's sans at the prose size (a boat by its name, IW-568), and the
   people and the evidence under it at the body size. The page's one emphatic
   thing is the selection bar, held at the foot of the window in the brand accent
   while anything is ticked, saying exactly what Confirm will do. Column and line
   measure follow the Learnings page, a little wider for the row's box. */
.history-review { max-width: 50rem; padding-bottom: calc(var(--space-xl) * 3); }
.history-review .reg-intro,
.history-review .rq-pii-note { max-width: 62ch; }
.history-review .reg-intro { font-size: var(--font-size-md); }
.hr-empty,
.hr-none { color: var(--color-text-muted); font-size: var(--font-size-md); max-width: 62ch; }
.hr-empty[hidden],
.hr-none[hidden] { display: none; }
.hr-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  margin: var(--space-lg) 0 var(--space-sm);
  font-size: var(--font-size-sm);
}
.hr-all { display: inline-flex; align-items: center; gap: var(--space-sm); color: var(--color-text-secondary); }
.hr-filter { flex: 1 1 14rem; max-width: 22rem; height: var(--control-height); padding: 0 var(--space-sm); }
.hr-shown { color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.hr-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--color-border-subtle); }
.hr-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-sm);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-md);
}
.hr-row[hidden] { display: none; }
.hr-row-selected { background: var(--color-bg-selected); }
.hr-check { margin-top: 4px; width: 16px; height: 16px; accent-color: var(--color-accent); }
.hr-body { min-width: 0; max-width: 62ch; }
.hr-name {
  display: block;
  font-size: var(--font-size-prose);
  font-weight: 600;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
  cursor: pointer;
}
.hr-people,
.hr-evidence,
.hr-note,
.hr-status { margin: 2px 0 0; }
.hr-people { color: var(--color-text-secondary); }
.hr-evidence { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.hr-evidence a { color: var(--color-text-secondary); }
.hr-note { color: var(--color-text-secondary); font-size: var(--font-size-sm); }
/* A row the bulk Confirm leaves out says why in the warning tone: it is waiting on
   a decision the box cannot make, not a fault. */
.hr-note-held { color: var(--color-status-warning); }
.hr-note a,
.hr-note .btn-link { color: var(--color-accent); opacity: 1; padding: 0; font-size: inherit; text-decoration: underline; }
.hr-status { font-size: var(--font-size-sm); }
.hr-status:empty { display: none; }
/* The boat a settled row became or joined, linked in the row's own tone, so it
   reads as part of what happened and not as the browser's default blue. */
.hr-status a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* A row's people (IW-647): closed, one quiet link-coloured line under the
   evidence, so the ledger still reads as a ledger; open, the New boat card's
   People on this boat block at the row's own text size. */
.hr-people-edit {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-sm);
  /* Not a brief section: the section frame from the details rules near the top
     of the file would draw a box inside a hairline ledger row. */
  border: none;
  border-radius: 0;
}
.hr-people-edit[hidden] { display: none; }
/* A row action, so it reads in the row's own sentence case rather than the
   brief section header register the details rules give every summary. */
.hr-people-edit > summary {
  width: fit-content;
  padding: 0;
  background: none;
  border-radius: 0;
  font-size: inherit;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-accent);
  cursor: pointer;
}
.hr-people-edit > summary::before { color: var(--color-text-muted); }
.hr-people-edit[open] > summary { color: var(--color-text-secondary); }
.hr-people-edit .rq-people { margin-top: var(--space-xs); }
/* Settled rows stay as their record, quieter, with what happened in the resolved
   tone; a row refused keeps its box and says why in the fault tone. */
.hr-row-settled .hr-name,
.hr-row-settled .hr-people,
.hr-row-settled .hr-evidence,
.hr-row-settled .hr-note { color: var(--color-text-muted); }
.hr-row-settled .hr-status { color: var(--color-status-resolved); }
.hr-row-failed .hr-status { color: var(--color-status-fault); }
.hr-bar {
  position: sticky;
  bottom: var(--space-md);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-lg);
  padding: var(--space-md) var(--space-lg);
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
  font-size: var(--font-size-sm);
}
.hr-bar[hidden] { display: none; }
.hr-bar-count { flex: 1 1 14rem; color: var(--color-text-secondary); }
.hr-bar-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
.hr-bar-actions [hidden] { display: none; }
.hr-bar-actions .btn-primary,
.hr-bar-actions .btn-secondary { min-height: var(--control-height); }
.hr-bar-status { flex: 1 1 100%; color: var(--color-text-muted); }
.hr-bar-status:empty { display: none; }
@container pane (max-width: 560px) {
  .hr-row { padding: var(--space-md) 0; }
  .hr-filter { max-width: none; }
  .hr-bar { bottom: 0; border-radius: 0; margin-left: calc(-1 * var(--space-md)); margin-right: calc(-1 * var(--space-md)); }
  .hr-bar-actions { width: 100%; }
  .hr-bar-actions .btn-primary { flex: 1 1 auto; }
}
/* Boat names (IW-652): the ledger with its row actions. Every boat name not on
   file is one row, and every row carries the same three actions against the
   right edge, where the eye runs down a column of them: Add, the row's one
   bordered control, then Add to… and Erase as links, Erase in the fault tone
   because it is the one action here that cannot be undone. The name leads its
   line with the ticket count beside it as data, which is what an operator
   weighs a name by. The page's emphatic thing is still the selection bar at
   the foot, so the row's own Add is the secondary button, never the accent. */
.bn-row { grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; }
.bn-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-md);
  margin: 0;
}
.bn-head .hr-name { display: inline; }
.bn-count,
.bn-origin { font-size: var(--font-size-sm); white-space: nowrap; }
.bn-count { color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
.bn-origin { color: var(--color-text-muted); }
.bn-actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-size: var(--font-size-sm);
}
.bn-actions[hidden],
.bn-actions [hidden] { display: none; }
/* .btn-link is coloured for the inverse header; these sit on the page. */
.bn-actions .btn-link { opacity: 1; padding: 0; font-size: inherit; }
/* While a row's Erase waits for its second click, the row offers only that and
   Cancel: the column keeps its width, so the row's lines do not re-wrap under
   the pointer, and nothing else can be pressed by mistake. */
.bn-row[data-confirming-at] .bn-add,
.bn-row[data-confirming-at] .bn-add-to { display: none; }
.bn-actions .bn-add-to { color: var(--color-text-secondary); }
.bn-actions .bn-add-to:hover { color: var(--color-text-primary); }
/* A proposed new boat keeps its full card, inside its row: the card's own
   frame at the row's width, a step under the row's lines. */
.bn-row .rq-card { margin: var(--space-sm) 0 0; max-width: none; }
/* Add to…: the boat search opens under the row's lines, with the button that
   sends it. */
.bn-pick { margin-top: var(--space-sm); font-size: var(--font-size-sm); }
.bn-pick[hidden] { display: none; }
.bn-pick-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-sm);
}
.bn-pick-actions .btn-primary,
.bn-pick-actions .btn-secondary { min-height: var(--control-height); }
/* On a phone the actions drop under the row's lines, on the text's own edge,
   and take the height a thumb needs. */
@container pane (max-width: 560px) {
  .bn-row { grid-template-columns: auto minmax(0, 1fr); }
  .bn-actions { grid-column: 2; flex-wrap: wrap; gap: var(--space-sm) var(--space-lg); }
  .bn-actions .btn-secondary { min-height: var(--control-height); }
  .bn-actions .btn-link { padding: var(--space-xs) 0; }
}
/* A team tip's wording, reworded before approving (IW-540). Closed, a link in the
   card's secondary tone under the claim; open, each line is a labelled box in the
   reason form's register, full width because a prompt runs to a few lines. */
/* A location's country, chosen with it (IW-753): a quiet label and the select on
   one line under the claim, on the chat card, a grouped card's row and the Boats
   queue card alike. The select sizes to the longest country name rather than to
   the card, like the trade select. */
.proposal-country,
.rq-country {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.proposal-country-select,
.rq-country-select {
  width: auto;
  max-width: 100%;
  font-size: var(--font-size-sm);
  font-weight: 400;
}
.proposal-wording { margin-top: var(--space-xs); }
.proposal-wording-open {
  color: var(--color-text-secondary);
  opacity: 1;
  font-size: var(--font-size-xs);
}
.proposal-wording-open:hover { color: var(--color-text-primary); }
.proposal-wording-open[hidden],
.proposal-wording-fields[hidden] { display: none; }
.proposal-wording-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-width: 36rem;
}
.proposal-wording-note { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.proposal-wording-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.proposal-wording-input {
  padding: 5px 8px;
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 400;
  color: var(--color-text-primary);
  resize: vertical;
}

/* Statusline + composer dock — the transcript scrolls above this; the dock stays put. */
.chat-statusline,
.command-chips,
.composer-wrap { flex: none; }
.chat-statusline {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-xs) 0;
  border-top: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
/* The row's one sizing contract (IW-395): every control is its own width, and the
   progress indicator is the single thing that takes the slack. Without this the model
   picker takes it, because the product's form default gives every select a width of
   100% and a flex row reads that as its wish, so the picker stretched across the row
   and squeezed Show detail onto two lines whenever the indicator's text grew. */
.chat-statusline > :not(.status-progress) { flex: none; }
.chat-statusline .model-select { width: auto; }
.composer-wrap { padding-bottom: var(--space-md); }

/* ── Technical data reads in mono ──────────────────────────────────────────────
 * Ticket ids, fault codes, the token readout, the tool summary, and command
 * names are codes and instrument readings, so they render in IBM Plex Mono. This
 * is the workbench's instrument-panel character; prose stays in Plex Sans. */
.chat-header .ticket-id,
.status-usage,
.tool-group-head, .tg-summary,
.cmd-chip {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
}
.status-usage { font-size: var(--font-size-xs); white-space: nowrap; }
.model-select {
  font-size: var(--font-size-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-xs);
}
.backstage-toggle {
  margin-left: auto;
  background: none;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  padding: 2px var(--space-sm);
  cursor: pointer;
  white-space: nowrap;
}
.backstage-toggle:hover { color: var(--color-text-secondary); }
.backstage-toggle[aria-pressed="true"] { color: var(--color-text-secondary); border-color: var(--color-text-muted); }
.status-usage { margin-left: var(--space-md); }

/* The statusline at a narrow pane (IW-391). Every control in the row is its own
   width (the contract above), so once the pane is narrower than their sum the row
   does not wrap, it clips, and the progress slot has already gone to zero before
   that. The only remedy is to take something out, and the readout is the reading
   the operator can spare: chat.js builds it as two spans so the stylesheet can
   drop the cached count first and the whole readout second. The steps are the
   pane's own two, named on .case-pane. At the narrow step the fixed controls
   total about 376px and the slot keeps the bar and a few words of its label down
   to a 620px pane, the width a 900px window leaves beside the expanded rail. At
   the tight step they total about 310px, and the empty tier and mode badges
   give up the gap they were holding; the open badge keeps its place because it
   is a live region and must exist before it speaks. */
@container pane (max-width: 720px) {
  .usage-cached { display: none; }
}
@container pane (max-width: 560px) {
  .status-usage { display: none; }
  .status-tier:empty,
  .status-mode:empty:not(.status-open) { display: none; }
}

/* Active-tier badge (R10): a quiet chip; the full tier and any manual override read at a
   glance. Empty until the first turn sets a tier. A proposal card's type tag is the same
   chip (IW-360), so a restyle of one is a restyle of both. */
.status-tier,
.proposal-tag {
  font-size: var(--font-size-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 1px var(--space-xs);
  color: var(--color-text-muted);
}
.status-tier:empty { border: none; padding: 0; }
/* Registry families (IW-529): a registry proposal card's chip takes the colour of the
   kind of change it proposes, so four cards from one brief read apart before a headline
   is read. The card carries the family as a class (proposal_card.js in the chat, the
   template on the Boats page) and the chip is the only thing that reads it.
   Words and outline take the colour and nothing is filled, which keeps it at chip
   weight and unlike a status pill. It is not a status: the left stripe still says
   pending or settled, in .proposal-card and .proposal-settled, and no rule here touches
   it, so a settled card keeps its family and loses only its warning edge. A card with
   no family class, every other proposal kind, keeps the quiet chip above.
   A row of a grouped card (IW-528) carries the family class itself and has no chip, so
   its mark takes the colour in the chip's place, outline only like the chip. */
.proposal-family-boat .proposal-tag,
.proposal-family-boat .proposal-row-mark { color: var(--color-family-boat); border-color: currentColor; }
.proposal-family-link .proposal-tag,
.proposal-family-link .proposal-row-mark { color: var(--color-family-link); border-color: currentColor; }
.proposal-family-controller .proposal-tag,
.proposal-family-controller .proposal-row-mark { color: var(--color-family-controller); border-color: currentColor; }
.proposal-family-lifecycle .proposal-tag,
.proposal-family-lifecycle .proposal-row-mark { color: var(--color-family-lifecycle); border-color: currentColor; }
.status-tier.tier-full { color: var(--color-text-primary); border-color: var(--color-text-muted); }
.status-tier.tier-manual { color: var(--color-text-secondary); border-style: dashed; }

/* Start-mode badge (case-memory U4): quiet chip, same family as the tier badge; shown
   only when the open rehydrated from case memory instead of resuming. */
.status-mode {
  font-size: var(--font-size-xs);
  border: 1px dashed var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 1px var(--space-xs);
  color: var(--color-text-muted);
}
.status-mode:empty { border: none; padding: 0; }
.status-mode.mode-rehydrated-degraded { color: var(--color-text-secondary); }

/* The progress indicator (IW-395, replacing the IW-282 meter's toggling): one element
   for every running turn, laid out at all times. It takes the whole middle of the
   statusline between the badges and Show detail, so it grows and shrinks with the
   window and never with its own content: the bar is a fixed width, the label is one
   line that truncates, and idle hides it in place rather than removing it. Nothing
   either side of it ever moves.

   States, set as classes by chat.js:
     is-active         a turn is running; without it the slot is invisible but present
     is-determinate    the bar carries a real fraction (a brief build with a count)
     is-indeterminate  work is running with nothing to divide yet: a short segment
                       travels the track, claiming no position
     is-waiting        the turn is parked on the operator's answer: the track is lit
                       with no moving edge, so it reads as on but not busy
     is-failed         the build stopped in words; the label steps up one tone and the
                       track goes still the same way, since a segment travelling under
                       "did not finish" would contradict the sentence

   The label is a sentence the operator reads, so it takes the row's own sans face
   and size. Mono stays reserved for the token readout beside it, which is a reading.

   The chat's loading notice (IW-538) draws the same bar, so its track and fill are
   named beside the statusline's in each rule below: is-loading travels as
   is-indeterminate does, is-slow is the still track, and reduced motion stills both. */
.status-progress {
  position: relative; /* the parent .visually-hidden needs, for the screen reader words */
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  visibility: hidden;
}
.status-progress.is-active { visibility: visible; }
.chat-loading-bar,
.status-progress-bar {
  flex: none;
  position: relative;
  display: inline-block;
  width: 8rem;
  height: 4px;
  border-radius: 2px;
  background: var(--color-border-subtle);
  overflow: hidden;
}
.chat-loading-fill,
.status-progress-fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--color-text-secondary);
  transition: width 200ms ease-out;
}
.chat-loading.is-loading .chat-loading-fill,
.status-progress.is-indeterminate .status-progress-fill {
  width: 30%;
  animation: wb-progress-travel 1.4s ease-in-out infinite;
  transition: none; /* or the segment tweens down from the last fraction while travelling */
}
/* is-failed sits after is-indeterminate so a failed beat, which carries no count and
   so is painted indeterminate, takes the still track. */
.chat-loading.is-waiting .chat-loading-fill,
.chat-loading.is-slow .chat-loading-fill,
.status-progress.is-waiting .status-progress-fill,
.status-progress.is-failed .status-progress-fill {
  width: 100%;
  opacity: 0.35;
  animation: none;
  transition: none;
}
@keyframes wb-progress-travel {
  from { transform: translateX(-100%); }
  to { transform: translateX(333%); }
}
.status-progress-label {
  flex: 1 1 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--color-text-muted);
}
.status-progress.is-failed .status-progress-label { color: var(--color-text-secondary); }
@media (prefers-reduced-motion: reduce) {
  .status-progress-fill { transition: none; }
  /* Without motion the travelling segment would sit still at one position, which is a
     claim. The lit track from the waiting state says "on" without saying "where". */
  .chat-loading.is-loading .chat-loading-fill,
  .status-progress.is-indeterminate .status-progress-fill {
    animation: none;
    width: 100%;
    opacity: 0.35;
  }
}

/* The composer row: paperclip, the message box, Send. The row aligns to its bottom
   edge, so as the box grows (below) the two buttons stay beside its last line rather
   than stretching to its full height. */
.chat-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  padding-top: var(--space-sm);
}
/* The message box grows with what is typed (IW-404). chat.js sets its height from
   the content on every input, so the operator can reread a longer instruction before
   sending it; the CSS holds the two bounds. One line is the floor, and the box is
   never taller than ten lines or four tenths of the window, whichever is smaller, so
   the transcript keeps most of the pane. Past that cap the text scrolls inside the
   box: overflow stays auto here, so the bar appears whenever the stylesheet clamps
   the box, including a window made shorter after the text was typed and a page whose
   script never ran. The line height is stated rather than inherited from the
   control's default because the floor and the cap are both built from it: the cap is
   ten line boxes plus the vertical padding plus the two borders, the same sum the
   script measures. No transition on the height: a box that eases open lags the
   cursor on every keystroke, which reads as jitter, the exact thing this replaces. */
.composer-input {
  flex: 1 1 auto;
  resize: none;
  font-family: var(--font-sans);
  font-size: var(--font-size-md);
  line-height: 1.5;
  padding: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
  min-height: calc(1.5 * var(--font-size-md) + 2 * var(--space-sm) + 2px);
  max-height: min(calc(10 * 1.5 * var(--font-size-md) + 2 * var(--space-sm) + 2px), 40vh);
  overflow-y: auto;
}
/* The command hint (IW-667): gray words after a bare command saying it takes a few
   words of steering, such as "focus (optional)" after /ticket-brief. The field is
   the box's own frame, so the layer over it is always the box's size, however the
   box grows. The layer restates the box's type, padding and border width, the sums
   that decide where a line breaks, so its unseen copy of the typed text wraps where
   the box wraps it and the hint starts where the caret stands. It takes no press:
   a click on the hint lands in the box beneath it. The colour is the muted role,
   the one the placeholder's kind of text already has, so the hint reads as an
   invitation and never as something typed. */
.composer-field {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
}
.composer-ghost {
  position: absolute;
  inset: 0;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--font-size-md);
  line-height: 1.5;
  letter-spacing: normal;
  padding: var(--space-sm);
  border: 1px solid transparent;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  color: var(--color-text-muted);
}
.composer-ghost[hidden] { display: none; }
.composer-ghost-typed { visibility: hidden; }
/* At rest the row is one line tall and both buttons stand as tall as the box's floor,
   the same sum as above, so the idle composer reads as one level row. The floor is
   restated rather than shared through a custom property because the stylesheet's
   tokens all live in tokens.css and a new one is its own card. */
.chat-composer .btn-attach,
.chat-composer .btn-primary {
  min-height: calc(1.5 * var(--font-size-md) + 2 * var(--space-sm) + 2px);
}

/* Attachments: paperclip button, staged tray, chips + thumbnails, drag-drop, error. */
.btn-attach {
  flex: none;
  padding: var(--space-sm);
  font-size: var(--font-size-md);
  line-height: 1;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-secondary);
  cursor: pointer;
}
.btn-attach:hover { color: var(--color-text-primary); border-color: var(--color-accent); }
.btn-attach svg { width: 16px; height: 16px; display: block; }
.attachment-tray {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  padding: var(--space-xs) 0;
}
.attachment {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  max-width: 16rem;
  padding: 2px var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.attachment-thumb {
  width: 40px;
  height: 40px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  display: block;
}
.attachment-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attachment-remove {
  flex: none;
  border: none;
  background: none;
  color: var(--color-text-secondary);
  font-size: var(--font-size-md);
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.attachment-remove:hover { color: var(--color-text-primary); }
/* IW-272: an image chip's one action. "In draft" in the pre-send tray is a toggle;
   "Keep" in a sent bubble opens the shared figure form. */
.attachment-use,
.attachment-keep {
  flex: none;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  padding: 0 var(--space-xs);
  cursor: pointer;
}
.attachment-use[aria-pressed="true"] {
  border-color: var(--color-accent);
  color: var(--color-text-primary);
  font-weight: 600;
}
.attachment-use:hover,
.attachment-keep:hover { color: var(--color-text-primary); }
/* IW-914: a chip still sending. The picture shows the moment it is pasted, drawn from
   the browser's own copy, so the thumbnail is the confirmation and stays at full
   strength. What is left to say is that it has not reached the server yet, and that is
   said with the statusline's own travelling segment (IW-395) along the chip's foot,
   gone the moment the server has the file. Reduced motion stills it to the lit track,
   as the statusline does. */
.attachment.is-sending {
  position: relative;
  overflow: hidden;
}
.attachment-sending {
  position: absolute;
  left: var(--space-xs);
  right: var(--space-xs);
  bottom: 0;
  height: 2px;
  border-radius: 1px;
  background: var(--color-border-subtle);
  overflow: hidden;
}
.attachment-sending-fill {
  display: block;
  width: 30%;
  height: 100%;
  background: var(--color-text-secondary);
  animation: wb-progress-travel 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .attachment-sending-fill {
    width: 100%;
    opacity: 0.35;
    animation: none;
  }
}
.draft-figures { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-top: var(--space-xs); }
.figure-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex-basis: 100%;
  margin-top: var(--space-xs);
  padding: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-sm);
}
.figure-form-hint { color: var(--color-text-muted); }
/* Share as a tip (IW-540), on a message the operator sent. It hangs just under the
   bubble's lower right corner, in the gap before the next message, so it never makes
   the bubble taller: hidden until the bubble is hovered or holds focus, the way the
   rail's Freshdesk link is, and always shown where there is no hover. Link styled in
   the muted tone, at the size of the timeline's quietest text. */
.msg-user { position: relative; }
.msg-share-tip {
  position: absolute;
  top: 100%;
  right: var(--space-xs);
  padding: 2px var(--space-xs);
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  line-height: 1.4;
  color: var(--color-text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
  cursor: pointer;
  /* Hidden is unclickable too, so a press in the gap under a bubble lands nowhere. */
  opacity: 0;
  pointer-events: none;
}
.msg-user:hover .msg-share-tip,
.msg-user:focus-within .msg-share-tip,
.msg-share-tip.is-shared { opacity: 1; pointer-events: auto; }
.msg-share-tip:hover { color: var(--color-accent); }
.msg-share-tip:disabled { text-decoration: none; cursor: default; color: var(--color-text-muted); }
.msg-user-unsent .msg-share-tip { display: none; }
/* Where there is no hover, the control is a line of the bubble's own, at its foot. */
@media (hover: none) {
  .msg-share-tip {
    position: static;
    display: block;
    margin: var(--space-xs) 0 0 auto;
    padding: var(--space-xs) 0 0;
    opacity: 1;
    pointer-events: auto;
  }
}
/* The share form opens under the bubble, at the bubble's side of the timeline, in the
   shared figure form's frame: the hint, the two lines, then the buttons. */
.tip-share-form {
  align-self: flex-end;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: min(36rem, 90%);
  margin-top: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-sm);
}
.tip-share-hint { color: var(--color-text-muted); font-size: var(--font-size-xs); }
.tip-share-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.tip-share-field input,
.tip-share-field textarea {
  padding: 5px 8px;
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 400;
  color: var(--color-text-primary);
}
.tip-share-field textarea { resize: vertical; }
.proposal-figure-image {
  display: block;
  max-width: 100%;
  max-height: 240px;
  margin-top: var(--space-xs);
  border-radius: var(--radius-sm);
}
.figure-confirm { display: flex; align-items: flex-start; gap: var(--space-xs); margin-top: var(--space-xs); }
/* Attachments shown inside a sent user bubble sit above its text. */
.msg-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-bottom: var(--space-xs);
}
.composer-wrap.drag-over {
  outline: 2px dashed var(--color-accent);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}
.upload-error {
  color: var(--color-status-fault, var(--color-text-primary));
  font-size: var(--font-size-sm);
  padding: var(--space-xs) 0;
}

/* Command palette: the action row (R14, IW-399) + slash autocomplete (R13).
   The row carries the phase-ranked actions as pills, one to three of them, and one
   More actions button that opens every other action as a menu. The pills keep the
   uniform pill shape and the instrument-panel mono; a suggested one takes the accent
   outline, the one chromatic note in the row, and a pill the server wants seen but
   not clicked (an essential skill missing from the seat) is greyed in place with its
   reason as the tooltip. The menu is the same raised popover the header's user menu
   and the rail's filter popover use, opening upward because the row sits at the foot
   of the pane above the composer. A greyed action in the menu carries its reason on
   a quiet second line, so the operator learns what unlocks it without a hover. */
.command-chips { display: flex; flex-wrap: wrap; gap: var(--space-xs); padding: var(--space-xs) 0; }
.cmd-chip {
  font-size: var(--font-size-sm);
  padding: 2px var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  background: var(--color-bg-surface);
  color: var(--color-text-secondary);
  cursor: pointer;
  white-space: nowrap;
}
.cmd-chip:hover { background: var(--color-bg-hover); }
.cmd-chip-suggested { border-color: var(--color-accent); color: var(--color-accent); }
.cmd-chip-disabled { opacity: 0.45; cursor: not-allowed; }
.action-menu { position: relative; display: inline-flex; }
.action-menu-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.action-menu-button[aria-expanded="true"] { background: var(--color-bg-hover); color: var(--color-text-primary); }
.action-menu-caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 5px solid currentColor;
}
.action-menu-button[aria-expanded="true"] .action-menu-caret { transform: rotate(180deg); }
.action-menu-list {
  position: absolute;
  bottom: 100%;
  left: 0;
  z-index: 21;
  min-width: 220px;
  max-width: calc(100vw - 2 * var(--space-lg));
  margin: 0 0 var(--space-xs);
  padding: var(--space-xs) 0;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
}
.action-menu-list[hidden] { display: none; }
.action-menu-item {
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--color-text-primary);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  letter-spacing: 0.02em;
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  white-space: nowrap;
}
.action-menu-item:hover, .action-menu-item:focus { background: var(--color-bg-hover); }
.action-menu-item:focus-visible { outline-offset: -2px; }
.action-menu-item[aria-disabled="true"] { color: var(--color-text-muted); cursor: not-allowed; }
.action-menu-item[aria-disabled="true"]:hover { background: none; }
.action-menu-reason {
  display: block;
  margin-top: 2px;
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  letter-spacing: 0;
  color: var(--color-text-muted);
  white-space: normal;
}
.action-menu-sep { height: 1px; margin: var(--space-xs) 0; background: var(--color-border-subtle); }
.composer-wrap { position: relative; }
.cmd-autocomplete {
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  margin-bottom: var(--space-xs);
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-raised);
  max-height: 16em;
  overflow-y: auto;
  z-index: 20;
}
/* A row is the command as the operator would type it, then what it does (IW-667).
   The command and its hint are one unit that never breaks: the name in
   the primary colour at weight 600, the hint beside it in the muted colour at the
   regular weight, so it reads as the quiet part of the same line. The list is one
   grid and each row borrows its columns, so every description starts on the same
   edge however long the command before it is. The description is the part that
   gives: it keeps to one line and is cut with an ellipsis, and keeps at least 10em.
   The hint shows whole on its one line wherever the row can hold it beside that
   (IW-769); only a row too narrow for it, in practice a phone, cuts it with an
   ellipsis. */
.cmd-autocomplete:not([hidden]) {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(10em, 1fr);
}
.cmd-option {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  column-gap: var(--space-md);
  align-items: baseline;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: var(--space-xs) var(--space-sm);
  cursor: pointer;
}
.cmd-option:hover { background: var(--color-bg-hover); }
.cmd-call { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cmd-name { font-weight: 600; color: var(--color-text-primary); }
.cmd-hint { font-weight: 400; color: var(--color-text-muted); }
.cmd-desc {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
/* In a tight pane, a phone or a narrow split, the command and its hint take the row
   and the description sits under them, still one line. The longest hint (/ask-docs)
   runs about 640px with its name, so a hint in a pane near this step may still be cut. */
@container pane (max-width: 720px) {
  .cmd-autocomplete:not([hidden]) { grid-template-columns: minmax(0, 1fr); }
}

/* Scoped queue rail entries (U10, status icons IW-48): presence + precedence only */
/* A row is the open button plus a sibling Freshdesk link (IW-286), never a link inside
   the button. The link shows on row hover or focus. */
.rail-row { position: relative; display: flex; align-items: center; }
.rail-row .rail-entry { flex: 1 1 auto; min-width: 0; }
.rail-fd {
  flex: none;
  padding: 0 var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-decoration: none;
  opacity: 0;
}
.rail-row:hover .rail-fd,
.rail-row:focus-within .rail-fd { opacity: 1; }
.rail-fd:hover { color: var(--color-accent); }
@media (hover: none) {
  .rail-fd { opacity: 1; }
}
.rail-entry {
  /* Holds any visually hidden text a row carries inside the scrolling rail;
     positioned against anything further out, it would grow the page. */
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  /* Every row is the height of a row with a boat (IW-617), so the list keeps one
     rhythm whether or not a ticket has a boat name yet. That height is the boat
     line, in the row's own font (1lh, since the button keeps the browser's control
     font rather than the page's), plus the person line under it, plus the padding.
     Everything in the row is centred in it, so the glyph, number, age and dot sit
     at the same height in every row and a row with no boat centres its one line.
     The line height is a number, not the button's `normal`, so a glyph drawn from
     a fallback font (an emoji in a boat name) cannot make its line taller than 1lh. */
  line-height: 1.2;
  min-height: calc(1lh + var(--font-size-xs) * 1.35 + 2 * var(--space-xs));
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--space-xs) var(--space-sm);
  cursor: pointer;
  color: var(--color-text-secondary);
}
.rail-entry:hover { background: var(--color-bg-hover); }
.rail-entry-mine { color: var(--color-text-primary); }
/* The ticket open in the main view (IW-166): a filled row with an accent edge, so it
   reads at a glance and survives the collapsed icon-only mode where text is hidden. */
.rail-entry-active,
.reg-suggest-active {
  background: var(--color-bg-hover);
  color: var(--color-text-primary);
  box-shadow: inset 2px 0 0 var(--color-accent);
}
.rail-glyph { flex: none; width: 15px; height: 15px; }
.rail-glyph svg { width: 100%; height: 100%; display: block; }
/* Whose-move state colors: chromatic only where action lives. Two states ask
   something of the operator and both carry a hue: amber for a customer waiting
   on a reply, the accent for a ticket nobody has answered at all. The rest stay
   quiet. `new` used to sit in the muted grey with `resolved` back when it only
   meant the operator had no local case row and half the queue wore it; now that
   it means genuinely unanswered it is the rail's rarest and sharpest state and
   must not read as finished (IW-101). `fresh` and `reply` are the same
   obligation from the operator's side (a reply is owed) and now share one
   glyph in rail.js, so colour alone carries the distinction here: amber says
   the customer wrote back, accent says nobody has answered at all (IW-200). */
.st-reply  { color: var(--color-status-warning); }
.st-wait   { color: var(--color-status-waiting); }
.st-done   { color: var(--color-text-muted); }
/* Quiet on purpose (IW-201): a captured case asks nothing of the operator, so
   it takes the same muted grey as done rather than the colour retired pm
   used, and is told apart by glyph alone. Colour stays reserved for the two
   states that ask something: amber for a reply owed, accent for unanswered. */
.st-captured { color: var(--color-text-muted); }
.st-fresh  { color: var(--color-accent); }
.rail-num {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.rail-cust { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A row whose ticket has a boat (IW-606): the boat on the first line and the person
   under it, smaller and muted. The pair is centred in the row like everything else
   (IW-617); a row with no boat shows the person alone on one line, centred. The
   person's line height is pinned because .rail-entry's height is built from it. */
.rail-who {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
/* The first line is the ticket's subject (IW-879): its boat, else its partner
   company, else its customer, all in the row's own type, since the rail is read
   by the line, not the kind. The person, and a quiet "started by" note when a
   team member started the ticket, share the second line as one phrase ("Jan
   Smit, started by Dana Ortiz"). The line is one line tall and wraps out of
   sight, so where both do not fit the note leaves whole rather than as a stub;
   the person keeps the line and gives way only to its own ellipsis. */
.rail-lead { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  min-width: 0;
  height: calc(var(--font-size-xs) * 1.35);
  overflow: hidden;
  font-size: var(--font-size-xs);
  line-height: 1.35;
  color: var(--color-text-muted);
}
.rail-who .rail-sub .rail-cust { flex: 0 1 auto; min-width: 0; max-width: 100%; }
.rail-started {
  flex: none;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: pre;
}
/* The Integrel mark (IW-619): a ticket a team member started whose customer could
   not be settled names that team member, and this word says so, on the rail row
   and beside the header's name. One outlined word in the muted register, never a
   colour, because colour on the rail means whose move it is. The name truncates
   before the mark does, so the mark is never the part cut off. */
.rail-cust-staff { display: flex; align-items: center; gap: var(--space-xs); }
.rail-cust-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.staff-mark {
  flex: none;
  padding: 0 4px;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--color-text-muted);
  white-space: nowrap;
}
.staff-mark[hidden] { display: none; }
.chat-header .staff-mark { align-self: center; }
/* A boat page person whose link was ended (IW-619) reads its reason in the
   muted register, after the role. */
.reg-person-ended { color: var(--color-text-muted); }
.rail-who .rail-cust {
  flex: none;
  font-size: var(--font-size-xs);
  line-height: 1.35;
  color: var(--color-text-muted);
}
/* The row's trailing slot: the last updated age, or under Priority and Due by
   time the value that put the row where it is (IW-370). One register for both
   on purpose: mono, muted, tabular, so a priority word or a due stamp reads as
   data and takes no colour. Colour on the rail means whose move it is. */
.rail-age, .rail-sorted {
  flex: none;
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  white-space: nowrap;
}
.rail-entry-unread .rail-num { font-weight: 700; }

/* Text a screen reader reads and the screen does not show (IW-393), such as the
   plain words behind the status line's nautical phrase. Out of flow, so it takes
   no slot in a flex gap; its parent must be positioned. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.rail-unread {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-accent);
  flex: 0 0 auto;
}
/* The WhatsApp mark (IW-580): a ticket that came in through WhatsApp carries a
   small chat bubble, beside the number in the ticket header and with the trailing
   markers on a rail row, so the customer names keep one column. Subtle by
   decision. The glyph is drawn in the rail's own stroke at the size of the text
   beside it, in the muted text colour, never WhatsApp's green and never
   a badge. The reply window's time beside it takes the trailing slot's register
   (mono, muted, tabular) and the warning colour only in the last two hours, the
   one moment it asks something of the operator. whatsapp.js fills both. */
.wa-mark {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}
.wa-icon { display: inline-flex; width: 1.1em; height: 1.1em; }
.wa-icon svg { width: 100%; height: 100%; display: block; }
.wa-window {
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.wa-window-closing { color: var(--color-status-warning); }
/* The short form is for a tight header only; display:none also keeps the
   unshown copy out of the accessible name. */
.wa-short { display: none; }
/* In the header the line is a phrase, not a datum, so it takes the header's own
   face rather than the rail's mono. The header sets its line on the text
   baseline; an icon has none, so the mark centres on the line instead of riding
   above it. A tight header takes the short form, so the mark keeps its width and
   the subject gives way first, as it always has. */
.chat-header .wa-mark { align-self: center; }
.chat-header .wa-window {
  font-family: inherit;
  font-size: inherit;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
}
@container pane (max-width: 560px) {
  .chat-header .wa-long { display: none; }
  .chat-header .wa-short { display: inline; }
}
/* The WhatsApp email card (IW-581): a strip under the ticket header, framed like
   the About this boat panel below it, since it is a fact about the customer rather
   than a turn in the chat. The notice is muted prose; each found address is the
   one thing set in the primary text colour, with where it came from beneath it in
   the muted register and its buttons at the trailing edge. The two settled states
   say themselves in the status colours: warning for an address another contact
   holds, which needs the operator in Freshdesk, and resolved once it is saved. */
.wa-email {
  flex: none;
  margin: var(--space-sm) 0 0;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.wa-email[hidden] { display: none; }
.wa-email p { margin: 0; }
.wa-email-notice { color: var(--color-text-secondary); }
.wa-email-offers { list-style: none; margin: 0; padding: 0; }
.wa-email-notice + .wa-email-offers { margin-top: var(--space-sm); }
.wa-email-offer {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-md);
  row-gap: 2px;
  align-items: start;
  padding: var(--space-sm) 0;
}
.wa-email-offer + .wa-email-offer { border-top: 1px solid var(--color-border-subtle); }
.wa-email-offers .wa-email-offer:first-child { padding-top: 0; }
.wa-email-offers .wa-email-offer:last-child { padding-bottom: 0; }
.wa-email-notice + .wa-email-offers .wa-email-offer:first-child {
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.wa-email-address {
  grid-column: 1;
  color: var(--color-text-primary);
  font-weight: 500;
  overflow-wrap: anywhere;
}
.wa-email-origin,
.wa-email-elsewhere,
.wa-email-saved { grid-column: 1; font-size: var(--font-size-xs); line-height: 1.45; }
.wa-email-origin { color: var(--color-text-muted); }
.wa-email-elsewhere { color: var(--color-status-warning); }
.wa-email-saved { color: var(--color-status-resolved); }
.wa-email-origin a,
.wa-email-elsewhere a { color: inherit; text-decoration: underline; }
.wa-email-actions {
  grid-column: 2;
  grid-row: 1 / span 3;
  display: flex;
  gap: var(--space-sm);
  align-self: center;
}
.wa-email-actions button { white-space: nowrap; }
.wa-email-message {
  margin-top: var(--space-sm);
  color: var(--color-status-warning);
  font-size: var(--font-size-xs);
}
/* A narrow pane puts the buttons under the words rather than squeezing both. */
@container pane (max-width: 560px) {
  .wa-email-offer { grid-template-columns: minmax(0, 1fr); }
  .wa-email-actions { grid-column: 1; grid-row: auto; margin-top: var(--space-xs); }
}
/* A message that reports a merge done takes the resolved colour; a refusal keeps
   the warning colour above. */
.wa-email-message[data-tone="done"] { color: var(--color-status-resolved); }

/* ── Merge contacts (IW-889) ──
   The preview a Merge contacts press opens: one other Freshdesk contact folded
   into the ticket's contact, which Freshdesk cannot undo. It is a dialog in the
   top layer over the dimmed screen, the shape of the reading form (.xf): raised
   surface, subtle border, the medium radius and the raised shadow, fluid width
   that on a phone fills the screen less a small margin. The one emphasized
   thing is the pair of contacts side by side under the lead sentence. Each is a
   nameplate with no labels: the name first, then the addresses, the numbers and
   the ticket count, since each reads as what it is. The contact that stays sits
   under a keyline in the resolved colour and the one that goes under one in the
   warning colour, with a small sentence case caption on each, so which survives
   is read before a word is. Under the pair, what the kept contact holds
   afterwards, then the one sentence that it cannot be undone in the warning
   colour, directly above Cancel and the product's ordinary primary button. A
   refusal is said in the fault colour in the same place. */
.wa-merge {
  width: min(36rem, calc(100vw - 2 * var(--space-sm)));
  max-width: none;
  max-height: min(46rem, calc(100dvh - 2 * var(--space-sm)));
  padding: 0;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  color: var(--color-text-primary);
  box-shadow: var(--shadow-raised);
  overflow: hidden;
}
.wa-merge::backdrop { background: var(--color-scrim); opacity: 0.7; }
.wa-merge[open] { display: flex; }
@media (prefers-reduced-motion: no-preference) {
  .wa-merge[open] { animation: photo-viewer-in var(--motion-enter) var(--ease-out); }
}
.wa-merge-form {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
  margin: 0;
  overflow: auto;
}
.wa-merge-head {
  position: relative;
  padding: var(--space-lg) clamp(var(--space-lg), 4vw, var(--space-xl)) var(--space-md);
  border-bottom: 1px solid var(--color-border-subtle);
}
.wa-merge-title {
  margin: 0;
  padding-right: var(--space-xl);
  font-size: var(--font-size-title);
  font-weight: 600;
  line-height: 1.3;
}
.wa-merge-lead {
  margin: 2px 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  line-height: 1.45;
}
.wa-merge-close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: var(--control-height);
  height: var(--control-height);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-muted);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.wa-merge-close:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
.wa-merge-pair {
  display: grid;
  /* Two columns where two fit, as on a desktop; on a phone the dialog is
     narrower than two of them, so the contacts stack with the one that stays
     first. The width is left to the work rather than keyed off the window,
     since the dialog sits in the top layer outside the pane's container. */
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-lg) var(--space-xl);
  padding: var(--space-lg) clamp(var(--space-lg), 4vw, var(--space-xl));
}
.wa-merge-contact {
  min-width: 0;
  padding-top: var(--space-sm);
  border-top: 2px solid var(--color-status-resolved);
}
.wa-merge-contact[data-role="goes"] { border-top-color: var(--color-status-warning); }
.wa-merge-caption {
  margin: 0;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-status-resolved);
}
.wa-merge-contact[data-role="goes"] .wa-merge-caption { color: var(--color-status-warning); }
.wa-merge-name {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-md);
  font-weight: 500;
  overflow-wrap: anywhere;
}
.wa-merge-name a { color: inherit; text-decoration: underline; text-decoration-color: var(--color-border-subtle); text-underline-offset: 2px; }
.wa-merge-name a:hover { text-decoration-color: currentColor; }
.wa-merge-facts {
  list-style: none;
  margin: var(--space-xs) 0 0;
  padding: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.wa-merge-facts li { overflow-wrap: anywhere; }
.wa-merge-email { color: var(--color-text-primary); }
.wa-merge-none { color: var(--color-text-muted); }
.wa-merge-foot {
  padding: var(--space-md) clamp(var(--space-lg), 4vw, var(--space-xl)) var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
  background: var(--color-bg-surface);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.wa-merge-foot p { margin: 0; }
.wa-merge-after { color: var(--color-text-secondary); }
.wa-merge-after .wa-email-address { font-weight: 500; }
.wa-merge-warning { margin-top: var(--space-sm); color: var(--color-status-warning); font-weight: 500; }
.wa-merge-message { margin-top: var(--space-sm); color: var(--color-status-fault); }
.wa-merge-message[hidden] { display: none; }
.wa-merge-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
/* Search results panel groups (IW-406): Queue, Worked cases, one per matched
   boat, All tickets. A group heading names where its rows came from, so it is
   sentence case and set in the row's own size rather than a shouted label; the
   space above it is what separates one group from the next. */
.rail-search-divider {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
  padding: var(--space-sm) var(--space-sm) 2px;
}
.rail-search-divider:first-child { padding-top: 0; }
/* A boat heading links its registry page; the owner beside it tells two boats
   of one name apart. */
.rail-search-boat a { color: var(--color-accent); text-decoration: none; }
.rail-search-boat a:hover { text-decoration: underline; }
.rail-search-owner { font-weight: 400; color: var(--color-text-muted); }
.rail-search-empty { color: var(--color-text-muted); font-size: var(--font-size-sm); padding: var(--space-xs) var(--space-sm); }
/* One result: the queue row's number, customer and age registers, so a ticket
   reads the same in the panel as in the rail, and the subject (or the matched
   case text) on a quieter second line under the customer. Like a rail row
   (IW-617), every result is the height of one with a subject, and the number and
   the meta are centred in it, so a result with no subject centres its one line
   and the columns stay level from result to result. The subject's gap is its own
   margin rather than a row gap, so an empty second row adds no height, and its
   line height is pinned because the result's height is built from it. */
.rail-row .rail-search-hit { flex: 1 1 auto; min-width: 0; }
.rail-search-hit {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  column-gap: var(--space-sm);
  align-content: center;
  align-items: center;
  min-height: calc(1lh + 2px + var(--font-size-xs) * 1.4 + 2 * var(--space-xs));
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--space-xs) var(--space-sm);
  cursor: pointer;
  color: var(--color-text-primary);
  font: inherit;
  font-size: var(--font-size-sm);
}
.rail-search-hit:hover { background: var(--color-bg-hover); }
.rail-search-hit :is(.rail-num, .rail-search-meta) { grid-row: 1 / span 2; }
/* With no subject the number and meta stay in the one row. Spanning an empty
   second row would hand it half the number's height, and the minimum height
   would let it keep that, lifting the line off centre. */
.rail-search-hit:not(:has(.rail-search-sub)) :is(.rail-num, .rail-search-meta) { grid-row: 1; }
.rail-search-who { grid-column: 2; grid-row: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-search-meta {
  grid-column: 3;
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  white-space: nowrap;
}
.rail-search-sub {
  grid-column: 2;
  grid-row: 2;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-xs);
  line-height: 1.4;
  color: var(--color-text-secondary);
}
/* What was and was not searched: Freshdesk held back, a cap that bound. */
.rail-search-note {
  margin-top: var(--space-xs);
  padding: var(--space-xs) var(--space-sm) 0;
  border-top: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.rail-search-note + .rail-search-note { margin-top: 0; border-top: none; }

/* ── Registry cards (IW-65 U13; on the Learnings page, IW-548, and the boat page) ──
 * Cards reuse the memory panel's proposal-card look; the page is a narrow
 * reading column inside the case pane. The Learnings link and its badge live
 * on the spine (IW-624). */
/* A count is a quiet neutral pill: it says how much is waiting, never that
   something is wrong. The one loud badge is the stuck fold, the case where a
   person has to look. Zero is not rendered at all. On the spine (IW-624) the
   pill sits on its icon's corner; .spine-badge places it. */
.nav-badge {
  background: var(--color-header-badge-bg);
  color: var(--color-text-inverse);
  border-radius: 999px;
  box-sizing: border-box;
  min-width: 18px;
  padding: 0 6px;
  font-size: var(--font-size-xs);
  line-height: 18px;
  font-weight: 500;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.nav-badge-alert {
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-weight: 600;
}
.registry-queue { max-width: 760px; margin: 0 auto; padding: var(--space-lg) 0; }
.rq-pii-note {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  margin: var(--space-xs) 0 var(--space-md);
}
.rq-empty { color: var(--color-text-muted); padding: var(--space-md) 0; }
.rq-group { margin-bottom: var(--space-lg); }
/* A group's heading names its boat the way an operator knows it, name and owner
   (IW-568), so it reads as words in the sans face; the key after it is the boat
   page's .reg-key, mono and muted, there to quote and never the name. */
.rq-boat {
  font-size: var(--font-size-md);
  font-family: var(--font-sans);
  font-weight: 600;
  text-transform: none;
  color: var(--color-text-primary);
  margin: 0 0 var(--space-xs);
  overflow-wrap: anywhere;
}
.rq-boat .reg-key { font-size: var(--font-size-xs); margin-left: var(--space-xs); }
.rq-card { background: var(--color-bg-surface); }
/* The card's boat, by name (IW-568): words in the sans face, not the mono a key took. */
.rq-card-boat { font-family: var(--font-sans); font-weight: 600; margin-right: var(--space-sm); }
/* The type label is the chat card's chip (IW-529), so it takes the family colour by
   the same rule, but it is this card's title and keeps a title's weight: the mono face
   and the weight .proposal-field gave the boat key beside it, at the card's own size.
   The chat chip is quiet because the headline under it names the type; here the chip
   is the only place the type is said. A type with no family reads in the primary tone
   inside the plain outline. */
.rq-type {
  margin-right: var(--space-sm);
  font-family: var(--font-mono);
  font-size: inherit;
  font-weight: 600;
  color: var(--color-text-primary);
}
.rq-status { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.rq-error { color: var(--color-status-fault); }
/* The collapsed-address toggle sits on a card, not the inverse header, so it
 * overrides .btn-link's inverse color. */
.rq-address-toggle { color: var(--color-text-secondary); opacity: 1; }
.rq-address-toggle:hover { color: var(--color-text-primary); }
.rq-address-value { display: block; margin-top: 2px; }
/* The people on a New boat card (IW-398). A quiet block under the card's own
 * rows, opened by the same hairline the candidate card's picker uses, so the
 * card still reads claim, then what the confirm settles, then the buttons.
 * Four rows on one shared grid, the boat page's label column narrowed to the
 * short labels here. Each row's answer is one dropdown (IW-576), and the four
 * share one answer column as wide as the widest answer on the card, capped by
 * the card, so they line up as a short form and the chosen answer reads in
 * full. Each row is a subgrid of that column; a browser without subgrid keeps
 * the row's own two columns. A "someone else" picker takes the column's width
 * under its row. The one emphasis is the outcome sentence at the foot, in the primary colour at the
 * card's size, exactly as .cr-outcome carries it, because it is what the
 * button will do. A fieldset carries browser chrome of its own (a border, a
 * min-width that ignores the column), so both are reset here. */
.rq-people {
  margin: var(--space-sm) 0 0;
  padding: var(--space-sm) 0 0;
  border: 0;
  border-top: 1px solid var(--color-border-subtle);
  min-width: 0;
}
.rq-people-title {
  padding: 0;
  margin: 0 0 var(--space-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.rq-people-rows {
  display: grid;
  grid-template-columns: 8rem minmax(0, max-content);
  column-gap: var(--space-md);
}
.rq-people-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  grid-template-columns: subgrid;
  gap: var(--space-xs) var(--space-md);
  align-items: baseline;
  margin-top: var(--space-xs);
}
.rq-people-label { color: var(--color-text-secondary); }
.rq-people-choices {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  min-width: 0;
}
.rq-choice {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
  color: var(--color-text-primary);
}
.rq-choice input { margin: 0; flex: none; }
.rq-choice[hidden] { display: none; }
/* Each row's dropdown (IW-576) fills the shared column, so all four end on
 * one edge. */
.rq-people-select { width: 100%; }
/* What opens or speaks under a dropdown takes the column's full width under
 * it: a "someone else" picker, the partner row's openers (IW-517, the trade
 * select under the requester's answer, the list under "From the service
 * partner list", one at a time), a note, the "works for this partner" tick. None of
 * them widens the column (IW-576): it is the dropdowns' width, and a long
 * note or an opened picker must not make all four jump wider. Scoped to the
 * card's rows, because the boat page's Record the installer form borrows
 * .rq-people-choices and .rq-choice for its inline radios. */
.rq-person-pick,
.rq-partner-pick { flex-basis: 100%; }
.rq-people-rows .rq-person-pick,
.rq-people-rows .rq-partner-pick,
.rq-people-rows .rq-people-choices > .rq-people-note,
.rq-people-rows .rq-people-choices > .rq-choice {
  flex-basis: 100%;
  contain: inline-size;
}
.rq-person-pick[hidden],
.rq-partner-pick[hidden] { display: none; }
/* The product's form default gives every select the width of its container;
 * these size to the names and trades they offer, like the Boats tab's mode select. */
.rq-partner-select,
.rq-installer-select,
.rq-trade-select { width: auto; max-width: 100%; }
/* The trade select's note is its own line under it: the answer column is the
 * dropdowns' width, too narrow for both side by side (IW-576). */
.rq-partner-trade .rq-people-note { display: block; margin-top: var(--space-xs); }
.rq-people-note { color: var(--color-text-muted); }
/* Why no owner was proposed (IW-516): one line under the title, before the rows. */
.rq-people-why { margin: 0 0 var(--space-xs); }
.rq-people-outcome {
  margin: var(--space-sm) 0 0;
  color: var(--color-text-primary);
  max-width: 38rem;
}
/* The two columns stack at the pane's tight step (IW-391). This was a window
 * query at 40rem, which with the rail expanded fired at a 360px pane, long after
 * the columns had squeezed; the pane's width is the one that matters here. */
@container pane (max-width: 560px) {
  .rq-people-rows,
  .rq-people-row { grid-template-columns: minmax(0, 1fr); }
}
/* The Suggest Docs update card's two page fields stack at the same step (IW-480).
 * Kept below the statusline's own block: tests read the first tight block as
 * the statusline's. */
@container pane (max-width: 560px) {
  .docs-update-fields { grid-template-columns: minmax(0, 1fr); }
}

/* ── Settings and operator console (IW-68) ────────────────────────────────────
 * "My settings" + /admin: a centered stack of instrument panels inside the
 * case pane. Each .admin-section echoes .card (the one surface shape); section
 * titles and column headers use the card-header tracked-uppercase register.
 * Tables drop the browser chrome: hairline row separators only, roomy rows,
 * ids / keys / timestamps / counts in Plex Mono so digits align.
 * The one deliberate moment is the breaker-panel status readout (state dot +
 * mono label): filled + tint halo when energized (set / OK / FAIL), hollow
 * ring when idle (not set / not tested). Status hues come from the existing
 * status token pairs; magenta stays reserved for the primary actions the
 * templates already mark with .btn-primary. */
.admin-console {
  max-width: 820px;
  margin: 0 auto;
  padding: var(--space-md) 0 48px;
}
.admin-header { margin-bottom: var(--space-xl); }
.admin-header h1 { margin: 0 0 var(--space-xs); }
.admin-sub {
  margin: 0;
  max-width: 60ch;
  font-size: var(--font-size-sm);
  line-height: 1.6;
  color: var(--color-text-muted);
}

/* Panels: quiet cards, generous rhythm between them. */
.admin-section {
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-lg) var(--space-xl) var(--space-xl);
  margin-bottom: var(--space-xl);
}
.admin-section h2 {
  margin: 0 0 var(--space-lg);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-secondary);
}
/* Optional right-aligned action shares the title baseline. */
.admin-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-lg);
}
.admin-section-head h2 { margin: 0; }
.admin-note {
  margin: var(--space-md) 0 0;
  max-width: 60ch;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* .btn-link is inverse-colored for the dark site header; on a console panel it
 * must read on the surface in both themes (it was white-on-white in light).
 * Same treatment .rq-address-toggle already uses on cards. */
.admin-console .btn-link { color: var(--color-text-secondary); opacity: 1; }
.admin-console .btn-link:hover { color: var(--color-text-primary); }
/* Destructive link actions (Deactivate, Sign out everywhere) read as fault. */
.admin-console .btn-link.admin-danger { color: var(--color-status-fault); }

/* Tables: no grid, no chrome. Hairline separators, first column flush with the
 * panel edge, trailing actions/amount column right-aligned. */
.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.admin-table th {
  padding: 0 var(--space-lg) var(--space-sm) 0;
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
.admin-table td {
  padding: 10px var(--space-lg) 10px 0;
  border-top: 1px solid var(--color-border-subtle);
  vertical-align: middle;
}
.admin-table th:last-child, .admin-table td:last-child {
  text-align: right;
  padding-right: 0;
}
/* The Models readout (IW-687): each tier's model as name, id and what chose it,
 * stacked in one cell, so a long dated id never splits across a third column. */
#admin-model-tiers td:last-child { white-space: nowrap; }
.admin-model-name { display: block; font-weight: 600; }
.admin-model-id { display: block; margin-top: 2px; }
.admin-model-source {
  display: block;
  margin-top: 2px;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
/* Usage (IW-20 U4, IW-323): an over-cap row carries the word "over cap" next to its
 * amount, so the blocked state survives greyscale and colour-blind viewing. */
.admin-spend-flag { margin-left: var(--space-sm); }
.admin-table tbody tr { transition: background-color 0.12s ease; }
.admin-table tbody tr:hover { background: var(--color-bg-hover); }
@media (prefers-reduced-motion: reduce) {
  .admin-table tbody tr { transition: none; }
}

/* Instrument readout cells: ids, setting keys, timestamps, turns, and costs in
 * mono with tabular numerals (the JS renders bare cells, so these are keyed to
 * each section's fixed column order). */
#admin-users td:first-child,
#admin-sessions td:first-child,
#admin-sessions td:nth-child(2),
#admin-sessions td:nth-child(3),
#admin-spend td:first-child,
#admin-spend td:nth-child(3),
#admin-spend td:nth-child(4),
#admin-spend td:nth-child(5) {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
/* My settings credential rows (IW-412): a plain name a teammate can read, the
 * storage key beside it in mono because the workbench's refusal messages name
 * that key, and a one-line instruction under both. The column is capped so the
 * instruction wraps instead of pushing the status and the input off the line. */
#my-credentials td:first-child { max-width: 34ch; vertical-align: top; }
#my-credentials input[type=email], #my-credentials input[type=text] { min-width: 22ch; }
.cred-name { font-weight: 600; color: var(--color-text-primary); }
.cred-key {
  margin-left: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
  color: var(--color-text-muted);
}
.cred-help {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.45;
  color: var(--color-text-secondary);
}
/* The Jira token's scopes (IW-426): one per line, read against the wizard's
 * checkboxes, so a plain mono column on a hairline rather than wrapped chips. */
.cred-scopes {
  margin: var(--space-sm) 0 0;
  padding: 0 0 0 var(--space-sm);
  list-style: none;
  border-left: 2px solid var(--color-border-subtle);
}
/* A scope is matched against the wizard letter for letter, so it never breaks
 * mid name; the column widens past its 34ch cap for the longest one instead. */
.cred-scopes code {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  line-height: 1.6;
  letter-spacing: 0.02em;
  color: var(--color-text-primary);
  white-space: nowrap;
}
/* A step's link lands on its row; mark the row it landed on. */
#my-credentials tr { scroll-margin-top: var(--space-xl); }
#my-credentials tr:target { background: var(--color-bg-selected); }
/* The Jira connection row (IW-380): the one primary action in the table, because
 * Atlassian's own consent page does the work the two paste fields used to ask of
 * the operator. A link wearing the button classes, since it navigates; the
 * Disconnect beside it is the table's link register in the fault colour. The
 * account it connected as reads in mono under the lamp, like the other readouts. */
.jira-connect-actions {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
}
.jira-connect-actions a { text-decoration: none; display: inline-block; line-height: 1.3; }
.jira-connect-actions a.btn-secondary { padding: 6px var(--space-lg); }
.cred-account {
  display: block;
  margin-top: var(--space-xs);
  max-width: 40ch;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
/* The row's readout is three lines (lamp, account, dates), so the status column is
 * capped and the dates stack under the account rather than running the column wide
 * enough to fold the other rows' inputs onto two lines. */
.jira-connection-row td:nth-child(2) { max-width: 30ch; }
.jira-connection-row .cred-when { display: block; margin: var(--space-xs) 0 0; }
/* The fallback (IW-380): the pasted Jira pair under one quiet line beneath the
 * table, closed unless the pair is what the seat runs on. Its own register, not the
 * brief's uppercase section summary: a sentence in the panel's text tone with the
 * seat setup's chevron at the right, on a hairline that continues the table's. */
.jira-fallback {
  margin: 0;
  border: none;
  border-top: 1px solid var(--color-border-subtle);
  border-radius: 0;
}
.jira-fallback > summary {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  padding: var(--space-md) 0;
  background: none;
  border-radius: 0;
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
  cursor: pointer;
  list-style: none;
}
.jira-fallback > summary:hover { color: var(--color-text-primary); }
.jira-fallback > summary::-webkit-details-marker { display: none; }
.jira-fallback > summary::before,
.jira-fallback[open] > summary::before { content: none; }
.jira-fallback > summary::after {
  content: "";
  width: 6px;
  height: 6px;
  margin-left: auto;
  border-right: 1.5px solid var(--color-text-muted);
  border-bottom: 1.5px solid var(--color-text-muted);
  transform: translateY(-2px) rotate(45deg);
}
.jira-fallback[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.jira-fallback > summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.jira-fallback-hint {
  font-size: var(--font-size-xs);
  font-weight: 400;
  color: var(--color-text-muted);
}
/* The rows inside keep the table's hairlines; the first sits under the summary. */
.jira-fallback > .admin-table { margin-bottom: var(--space-sm); }
/* On a phone the three columns cannot share the width: the instruction column
 * squeezed to a word a line and the status and input ran off the screen. Each
 * credential stacks instead, name and help, then status, then the input. */
@container pane (max-width: 560px) {
  #my-credentials .admin-table thead { display: none; }
  #my-credentials .admin-table,
  #my-credentials .admin-table tbody,
  #my-credentials .admin-table tr,
  #my-credentials .admin-table td { display: block; width: auto; }
  #my-credentials .admin-table tr {
    padding: var(--space-md) 0;
    border-top: 1px solid var(--color-border-subtle);
  }
  #my-credentials .admin-table td {
    max-width: none;
    padding: 0;
    border-top: none;
    text-align: left;
  }
  #my-credentials .admin-table td + td { margin-top: var(--space-sm); }
  #admin-model-tiers .admin-table thead { display: none; }
  #admin-model-tiers .admin-table,
  #admin-model-tiers .admin-table tbody,
  #admin-model-tiers .admin-table tr,
  #admin-model-tiers .admin-table td { display: block; width: auto; }
  #admin-model-tiers .admin-table tr {
    padding: var(--space-md) 0;
    border-top: 1px solid var(--color-border-subtle);
  }
  #admin-model-tiers .admin-table td {
    padding: 0;
    border-top: none;
    text-align: left;
    white-space: normal;
  }
  #admin-model-tiers .admin-table td + td { margin-top: var(--space-sm); }
  #my-credentials .seat-token-not-needed { margin-left: 0; }
  #my-credentials .my-cred-form { justify-content: flex-start; }
  #my-credentials .my-cred-form input { flex: 1 1 200px; }
  #my-credentials .jira-connect-actions { justify-content: flex-start; }
  #my-credentials .cred-account { max-width: none; }
  #my-credentials .jira-connection-row td:nth-child(2) { max-width: none; }
  /* The disclosure's hint takes its own line under the title, and the chevron
   * stays on the title line rather than dropping under the hint. */
  #my-credentials .jira-fallback > summary::after { order: 1; }
  #my-credentials .jira-fallback-hint { order: 2; flex-basis: 100%; }
}

/* Set up your seat (IW-426): a commissioning run. Numbered positions on one
 * hairline, in the order the credentials are accepted, each lit with the
 * energized lamp's colours once its credential is saved, and the final check
 * at the end of the run. A <details>, folded once all four are saved. */
/* Not the brief's section-summary register (details > summary): the panel's own
 * h2 is the heading, and the fold is a chevron at the right, not a triangle. */
.seat-setup > summary {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  padding: 0;
  background: none;
  border-radius: 0;
  cursor: pointer;
  list-style: none;
}
.seat-setup > summary::-webkit-details-marker { display: none; }
.seat-setup > summary::before,
.seat-setup[open] > summary::before { content: none; }
.seat-setup > summary h2 { margin: 0; }
.seat-setup > summary::after {
  content: "";
  width: 6px;
  height: 6px;
  margin-left: auto;
  border-right: 1.5px solid var(--color-text-muted);
  border-bottom: 1.5px solid var(--color-text-muted);
  transform: translateY(-2px) rotate(45deg);
}
.seat-setup[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.seat-setup > summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}
.seat-setup:not([open]) { padding-bottom: var(--space-lg); }
.seat-setup-count {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}
.seat-setup-steps {
  margin: var(--space-lg) 0 0;
  padding: 0;
  list-style: none;
  counter-reset: seat-step;
}
.seat-setup-step {
  position: relative;
  padding: 0 0 var(--space-lg) 36px;
  counter-increment: seat-step;
}
.seat-setup-step:last-child { padding-bottom: 0; }
.seat-setup-step::before {
  content: counter(seat-step);
  position: absolute;
  left: 0;
  top: -1px;
  width: 22px;
  height: 22px;
  box-sizing: border-box;
  border: 1.5px solid var(--color-text-muted);
  border-radius: 999px;
  background: var(--color-bg-surface);
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 19px;
  text-align: center;
  color: var(--color-text-secondary);
}
.seat-setup-step.is-saved::before {
  border-color: var(--color-status-resolved);
  background: var(--color-status-resolved-bg);
  color: var(--color-status-resolved);
}
/* Saved with a missing-scopes warning, on the page it was saved from. */
.seat-setup-step.is-warned::before {
  border-color: var(--color-status-warning);
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning);
}
/* The run: from under one position to the next. */
.seat-setup-step:not(:last-child)::after {
  content: "";
  position: absolute;
  left: 10px;
  top: 25px;
  bottom: 3px;
  width: 1.5px;
  background: var(--color-border-subtle);
}
.seat-setup-name {
  margin-right: var(--space-md);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-primary);
}
.seat-setup-auto { margin-left: 0; }
a.seat-setup-name { text-decoration: none; }
a.seat-setup-name:hover { text-decoration: underline; text-underline-offset: 2px; }
.seat-setup-step p {
  margin: var(--space-xs) 0 0;
  max-width: 62ch;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
/* Set by deploy (IW-449): each value's name in mono over its one-line description,
 * top-aligned so a wrapped description keeps its kind and value level with it. */
#admin-env-only td { vertical-align: top; }
#admin-env-only td:last-child { min-width: 8ch; }
#admin-env-only td:nth-child(2) {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
.env-only-name, .env-only-value {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
  overflow-wrap: anywhere;
}
.env-only-name { color: var(--color-text-primary); }
.cred-help a, .doc-reader-state a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* The documentation reader's state leads its sites section (IW-447). Off is the
 * warning panel, since a full list below it otherwise reads as on; on is one quiet
 * line with the energized lamp. */
.doc-reader-state[hidden] { display: none; }
.doc-reader-state p { margin: 0; line-height: 1.5; }
.doc-reader-state p + p { margin-top: var(--space-xs); }
.doc-reader-lead { font-weight: 600; }
.doc-reader-state code { font-family: var(--font-mono); font-size: var(--font-size-xs); }
.doc-reader-on {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
/* Turns, tokens and the figure after them (cost on a metered seat, the estimate on
 * a subscription seat) are numeric: right-align them. */
#admin-spend th:nth-child(3), #admin-spend td:nth-child(3),
#admin-spend th:nth-child(4), #admin-spend td:nth-child(4),
#admin-spend th:nth-child(5), #admin-spend td:nth-child(5) { text-align: right; }
/* Quiet metadata columns: role and status. */
#admin-users td:nth-child(2), #admin-users td:nth-child(3) {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
/* Deactivated accounts dim as a whole row, actions included. */
.admin-table tr.admin-row-inactive { opacity: 0.55; }

/* Breaker-panel readout: 8px state lamp + mono label. Filled with a tint halo
 * when the circuit is live, a hollow ring when idle. Shared by credential
 * status (.cred-*) and connection health (.health-*). */
.cred-set, .cred-unset, .cred-warn, .cred-bad, .health-status {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
}
.cred-set::before, .cred-unset::before, .cred-warn::before, .cred-bad::before,
.health-status::before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  margin-right: var(--space-sm);
  vertical-align: 1px;
}
.cred-set { color: var(--color-status-resolved); text-transform: uppercase; }
.cred-set::before {
  background: var(--color-status-resolved);
  box-shadow: 0 0 0 3px var(--color-status-resolved-bg);
}
.cred-unset { color: var(--color-text-muted); text-transform: uppercase; }
.cred-unset::before { box-shadow: inset 0 0 0 1.5px var(--color-text-muted); }
/* A credential that needs attention soon (a seat token in its last 30 days, or one
 * saved without its check), and one that no longer works (expired, unreadable). The
 * same lamp as .cred-set in the warning and fault colours (IW-381). */
.cred-warn { color: var(--color-status-warning); text-transform: uppercase; }
.cred-warn::before {
  background: var(--color-status-warning);
  box-shadow: 0 0 0 3px var(--color-status-warning-bg);
}
.cred-bad { color: var(--color-status-fault); text-transform: uppercase; }
.cred-bad::before {
  background: var(--color-status-fault);
  box-shadow: 0 0 0 3px var(--color-status-fault-bg);
}
/* The one line under a status that says what to do about it. */
.cred-detail {
  display: block;
  margin-top: var(--space-xs);
  max-width: 40ch;
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
.cred-when {
  margin-left: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* A time reads as one unit, so it wraps before it rather than inside it (IW-468). */
.cred-when time { white-space: nowrap; }
/* Health default covers "not tested" and the transient "testing..." state;
 * admin.js swaps in health-ok / health-bad after each probe. */
.health-status { color: var(--color-text-muted); }
.health-status::before { box-shadow: inset 0 0 0 1.5px var(--color-text-muted); }
.health-status.health-ok { color: var(--color-status-resolved); }
.health-status.health-ok::before {
  background: var(--color-status-resolved);
  box-shadow: 0 0 0 3px var(--color-status-resolved-bg);
}
.health-status.health-bad { color: var(--color-status-fault); }
.health-status.health-bad::before {
  background: var(--color-status-fault);
  box-shadow: 0 0 0 3px var(--color-status-fault-bg);
}

/* Inline value forms (credential + instance-setting rows): input and Save on
 * one row, right-justified into the trailing column; the input keeps a fixed
 * working width instead of stretching, and the pair wraps on narrow panes. */
.my-cred-form, .admin-setting-form {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
}
.my-cred-form input, .admin-setting-form input, .admin-setting-form select {
  width: 200px;
  min-width: 120px;
  flex: 0 1 200px;
  padding: 5px 8px;
}
/* The seat token row's Copy command button sits in the same line as the paste box,
 * so it takes the input's height rather than the full-size button's (IW-381). */
.seat-token-form .btn-secondary { padding: 5px var(--space-md); }

/* Action cluster in table rows (Edit role / Deactivate). */
.admin-actions .btn-link + .btn-link { margin-left: var(--space-lg); }

/* Enroll / edit form: a footnote panel under the users table. The hairline
 * spans the panel; the fields keep a working width. */
.admin-form {
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
}
.admin-form-row { max-width: 380px; margin-bottom: var(--space-md); }
.admin-form-row label {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.admin-optional { font-weight: 400; color: var(--color-text-muted); }
.admin-form-actions {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  margin-top: var(--space-lg);
}

/* Documentation start pages (IW-408). The form takes addresses, one per line,
 * so the row runs wider than a name field and the textarea is set in the panel's
 * mono: an address is an instrument reading, not prose. In the table the pages
 * are the crawl list of the row's host, shown as paths in the same mono, one per
 * line, since the host is the row's own and the path is the information. */
.admin-form-row-wide { max-width: 560px; }
.doc-site-pages-input {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  line-height: 1.6;
  resize: vertical;
  min-height: 5.2em;
}
.doc-site-pages-input::placeholder { color: var(--color-text-muted); }
.doc-site-pages-list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  line-height: 1.6;
  color: var(--color-text-secondary);
}
/* A path breaks after a slash (my_settings.js puts a break opportunity there) and
 * only inside a segment when one segment alone is wider than the cell. */
.doc-site-pages-list li { overflow-wrap: anywhere; }
.doc-site-pages-none {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* Pages kept from live reads (IW-502) close the crawl list: a tally under the
 * paths, in the line's own sans so it reads as a count of them and not as one
 * more path. The number is the reading, set in the paths' mono. Clear is the
 * plain link, not the fault one: Remove stays the row's one red action, and a
 * confirmation stands between this press and the delete. */
.doc-site-kept {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-md);
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.6;
  color: var(--color-text-muted);
}
.doc-site-kept-count {
  font-family: var(--font-mono);
  color: var(--color-text-secondary);
}
.admin-console .btn-link.doc-site-kept-clear { font-size: var(--font-size-xs); }
/* A site that turns away automated readers (IW-758) says so once, in a strip under
 * its row spanning the table, in the warning pair the page's other alerts use. It
 * is a sentence for the operator to act on (remove the site, or leave it), so it
 * is set at reading size and measure, not in the table's small print. */
#doc-sites-table tr.doc-site-blocked-row td { padding-top: 0; border-top: none; white-space: normal; }
.doc-site-blocked {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  max-width: 72ch;
  border-left: 3px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  text-align: left;
}
/* Uploaded manuals (IW-767). The tally sits under the crawl list in the kept
 * pages' own shape (a count in mono, then words, then the one control), so the
 * column reads as everything the site's library is built from. */
.doc-site-uploads {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-md);
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.6;
  color: var(--color-text-muted);
}
.doc-site-uploads-when { white-space: nowrap; }
/* The panel under a site spans the table like the blocked strip above it, and
 * is the one surface here that asks for an action: a raised tray with a dashed
 * edge, the shape of a place to put something. The edge takes the accent while
 * a file is held over it, which is the only motion it has. */
#doc-sites-table tr.doc-site-uploads-row td { padding-top: 0; border-top: none; white-space: normal; text-align: left; }
#doc-sites-table tr.doc-site-uploads-row:hover { background: none; }
.doc-upload-drop {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-lg);
  max-width: 72ch;
  padding: var(--space-md) var(--space-lg);
  border: 1.5px dashed var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.doc-upload-drop.is-over {
  border-color: var(--color-accent);
  background: var(--color-bg-selected);
}
@media (prefers-reduced-motion: reduce) {
  .doc-upload-drop { transition: none; }
}
.doc-upload-lead {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
}
.doc-upload-hint {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.doc-upload-drop > div { flex: 1 1 36ch; }
.doc-upload-pick { flex: none; }
/* One line per file of the last batch: the name in the paths' mono, what
 * happened to it in sans, coloured by the status pairs the page already uses. */
.doc-upload-progress {
  max-width: 72ch;
  margin: var(--space-sm) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--font-size-xs);
  line-height: 1.6;
}
.doc-upload-progress:empty { display: none; }
.doc-upload-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-md);
  padding: 2px 0;
  border-bottom: 1px solid var(--color-border-subtle);
}
.doc-upload-name {
  font-family: var(--font-mono);
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.doc-upload-state { color: var(--color-text-muted); }
.doc-upload-line.is-ok .doc-upload-state { color: var(--color-status-resolved); }
.doc-upload-line.is-scanned .doc-upload-state,
.doc-upload-line.is-same .doc-upload-state { color: var(--color-text-secondary); }
.doc-upload-line.is-refused .doc-upload-state { color: var(--color-status-fault); }
.doc-upload-summary { max-width: 72ch; margin: var(--space-sm) 0 0; }
/* The files on record, folded: a site carried over by hand holds hundreds. A
 * plain line in the table's sentence case, not the brief's card header that a
 * bare details wears: it is a list under a row, not a section of its own. */
.doc-upload-files {
  max-width: 72ch;
  margin: var(--space-sm) 0 0;
  border: none;
  font-size: var(--font-size-xs);
}
details.doc-upload-files > summary {
  padding: 0;
  background: none;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--color-text-secondary);
}
.doc-upload-files ul {
  max-height: 18rem;
  overflow-y: auto;
  margin: var(--space-xs) 0 0;
  padding: 0;
  list-style: none;
}
.doc-upload-files li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding: 2px 0;
  border-bottom: 1px solid var(--color-border-subtle);
}
.admin-console .doc-upload-files .btn-link { font-size: var(--font-size-xs); flex: none; }
.doc-site-blocked a { color: inherit; text-decoration: underline; }
/* The host and maker keep to one line on a desktop; the paths column takes the
 * width a long path needs and wraps, so it never pushes the actions off the panel. */
#doc-sites-table td:first-child, #doc-sites-table td:nth-child(2) { white-space: nowrap; }
#doc-sites-table .doc-site-pages { min-width: 18ch; max-width: 36ch; }
/* On a phone six columns cannot share the width. Each site stacks: host, then
 * maker, its start pages, note, when it was added, and Remove, each on its own
 * line; a cell with nothing in it takes no line. Same shape as #my-credentials. */
@container pane (max-width: 560px) {
  #doc-sites-table thead { display: none; }
  #doc-sites-table,
  #doc-sites-table tbody,
  #doc-sites-table tr,
  #doc-sites-table td { display: block; width: auto; }
  #doc-sites-table tr {
    padding: var(--space-md) 0;
    border-top: 1px solid var(--color-border-subtle);
  }
  #doc-sites-table td {
    max-width: none;
    padding: 0;
    border-top: none;
    text-align: left;
    white-space: normal;
  }
  #doc-sites-table td:empty { display: none; }
  #doc-sites-table td + td { margin-top: var(--space-xs); }
  #doc-sites-table td.admin-actions { margin-top: var(--space-sm); }
  #doc-sites-table td:first-child .cred-when { margin-left: 0; }
  #doc-sites-table td:first-child { overflow-wrap: anywhere; }
  /* The strip belongs to the site above it, not to a site of its own. */
  #doc-sites-table tr.doc-site-blocked-row { padding-top: 0; border-top: none; margin-top: calc(-1 * var(--space-xs)); }
  /* So does the upload panel, and its button takes the full width under the words. */
  #doc-sites-table tr.doc-site-uploads-row { padding-top: 0; border-top: none; }
  .doc-upload-drop { padding: var(--space-md); }
  .doc-upload-pick { width: 100%; }
}

/* ── Admin settings layout (IW-448) ───────────────────────────────────────────
 * The page is grouped by what an admin is doing, and every setting sits in the
 * panel of the thing it controls. Scoped to .admin-page: My settings shares
 * .admin-console and .admin-section and keeps its own look. The groups are listed
 * in the rail beside the spine (IW-624), one shown at a time, so the page itself
 * stays quiet. Headings are sentence case here; the tracked capitals stay on
 * table columns. */
/* A group: a plain heading and its one-sentence purpose, then its panels. */
.admin-group { scroll-margin-top: var(--space-lg); }
.admin-group-head { margin: 0 0 var(--space-lg); }
.admin-group-head h2 { margin: 0 0 var(--space-xs); color: var(--color-text-primary); }
.admin-group-intro {
  margin: 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* A panel: title and its actions on one line, quiet links first and the primary
 * action last, then what the panel is for, then its readouts and settings. */
.admin-page .admin-section-head { margin-bottom: var(--space-xs); }
.admin-page .admin-section h3 {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-primary);
}
.admin-section-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
}
.admin-panel-intro {
  margin: 0 0 var(--space-md);
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
/* A live state line (what the index holds, what the limits are): body text, since
 * it is the answer the panel exists to give. Empty until admin.js fills it. */
.admin-readout {
  margin: 0 0 var(--space-md);
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-primary);
}
.admin-readout:empty { display: none; }
/* Facts at a glance (the reply corpus's health, IW-560): a label column sized to its
 * longest label, kept short so the value beside it still has room on a phone. */
.admin-facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-xs) var(--space-md);
  margin: 0 0 var(--space-md);
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.admin-facts:empty { display: none; }
.admin-facts dt { color: var(--color-text-secondary); }
.admin-facts dd { margin: 0; color: var(--color-text-primary); }
.admin-facts .admin-fact-bad { color: var(--color-status-fault); }
.admin-corpus-warnings:empty { display: none; }
.cr-history-note a { color: var(--color-accent); }
.admin-corpus-warnings .alert { max-width: var(--measure-prose); }
.admin-page #admin-spend-metering { margin: 0 0 var(--space-md); }

/* Remove a customer (IW-944). The one admin control that cannot be undone, so it
 * reads as a docket of what Integra holds before anything goes: the person's name
 * and addresses, the counts in the facts grid, the boats that stay, and the ticket
 * numbers in mono, since those are copied into Freshdesk. The fault colour is kept
 * for the remove button and the confirm band alone; the band takes the action
 * row's place, with a fault rule down its side, so the second press sits where
 * the first one was. The receipt after a removal is drawn in the resolved tone. */
.rc-find { max-width: var(--measure-prose); margin: 0 0 var(--space-md); }
.rc-find label {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.rc-find-row { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.rc-find-row input { flex: 1 1 16rem; width: auto; min-width: 0; }
.rc-find-row button { flex: 0 0 auto; }
.rc-message {
  margin: 0 0 var(--space-md);
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.rc-message.is-refused { color: var(--color-status-fault); }
.rc-docket {
  max-width: var(--measure-prose);
  padding: var(--space-md) 0 0;
  border-top: 1px solid var(--color-border-subtle);
}
.rc-name {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-primary);
}
.rc-name.is-unnamed { font-weight: 400; color: var(--color-text-muted); }
.rc-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  margin: 0 0 var(--space-md);
  padding: 0;
  list-style: none;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.rc-list li { overflow-wrap: anywhere; }
.rc-sub {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.rc-none { margin: 0 0 var(--space-md); font-size: var(--font-size-sm); color: var(--color-text-muted); }
.rc-tickets { font-family: var(--font-mono); color: var(--color-text-primary); }
.rc-tickets li {
  padding: 1px var(--space-xs);
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
}
.rc-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-lg); }
/* The confirm band takes the action row's place, so the row must really go: its
 * display above would otherwise outrank the hidden attribute. */
.rc-actions[hidden], .rc-confirm[hidden] { display: none; }
.rc-confirm {
  max-width: var(--measure-prose);
  padding: var(--space-md) var(--space-lg);
  border-left: 3px solid var(--color-status-fault);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--color-status-fault-bg);
}
.rc-confirm p {
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-primary);
}
.rc-confirm strong { font-weight: 600; }
.rc-done {
  max-width: var(--measure-prose);
  padding: var(--space-md) 0 0;
  border-top: 1px solid var(--color-border-subtle);
}
.rc-done-title {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-status-resolved);
}
.rc-done.is-partial .rc-done-title { color: var(--color-status-warning); }
.rc-done p {
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-primary);
}

/* Evals (IW-811). Each suite is a hairline row like the admin tables, not a card: a
 * title with its one action, then one sentence of facts. The strip is the panel's one
 * figure. Its band is the release run's spread from worst to best run, its tick a run
 * from here, so "inside the band" reads as "within the noise" before any number is
 * read. The scale starts a tenth below the lowest value and is printed at both ends,
 * since scores cluster near the top. The confirm carries the accent rule because it is
 * the one place on the panel that spends money. */
.eval-suites { list-style: none; margin: 0; padding: 0; max-width: var(--measure-wide); }
.eval-suite {
  padding: var(--space-md) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.eval-suite:last-child { padding-bottom: 0; }
.eval-suite-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-md);
  flex-wrap: wrap;
}
.eval-suite-title { margin: 0; font-size: var(--font-size-md); font-weight: 600; }
.eval-suite-facts {
  margin: 2px 0 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.eval-stopped {
  margin: var(--space-sm) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-status-warning);
}
.eval-progress {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-sm);
  font-size: var(--font-size-sm);
}
.eval-progress progress { width: 14rem; max-width: 100%; height: 6px; accent-color: var(--color-accent); }

.eval-strip { margin: var(--space-md) 0 0; max-width: 40rem; }
.eval-strip-track {
  position: relative;
  height: 14px;
  background: linear-gradient(var(--color-ledger-rail), var(--color-ledger-rail))
              center / 100% 1px no-repeat;
}
.eval-strip-band {
  position: absolute;
  top: 3px;
  bottom: 3px;
  background: var(--color-bg-hover);
  border: 1px solid var(--color-ledger-rail);
  border-radius: var(--radius-sm);
}
.eval-strip-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 3px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--color-text-primary);
}
/* The scale's two ends sit under the track's ends, and the sentence has its
 * own line beneath, so on a phone it never squeezes between them. */
.eval-strip-scale {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.eval-strip-caption {
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}

/* The compare and the report fold away like the upload list's, in sentence case:
 * they are a run's detail, not brief sections, so they leave that register. */
details.eval-detail { margin: var(--space-sm) 0 0; border: none; font-size: var(--font-size-sm); }
details.eval-detail > summary {
  padding: 0;
  background: none;
  font-size: var(--font-size-sm);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--color-text-secondary);
}
details.eval-detail > summary .badge { margin-left: var(--space-xs); }
details.eval-detail[open] > summary { margin-bottom: var(--space-sm); }
.eval-detail-line { margin: 0 0 var(--space-xs); max-width: var(--measure-prose); }
.eval-misses { margin-top: var(--space-sm); }
.eval-report { max-height: 22rem; overflow: auto; font-size: var(--font-size-xs); }

.eval-confirm {
  margin-top: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  max-width: var(--measure-prose);
  background: var(--color-bg-page);
  border-left: 2px solid var(--color-accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.eval-confirm-ask { margin: 0; font-weight: 600; }
.eval-confirm-cost { margin: var(--space-xs) 0 var(--space-md); font-size: var(--font-size-sm); }
.eval-confirm-actions { display: flex; align-items: center; gap: var(--space-lg); }
.eval-confirm-msg {
  margin: var(--space-sm) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-status-fault);
}

/* Agent traces (IW-782): one ticket's turns, each a fold that opens to the turn in
 * order. The open turn borrows the transcript's ledger: a hairline rail down the
 * steps, with a tick where the agent acted (a tool, a gate, an approval, a write) and
 * none where it wrote or was asked, so the shape of the turn reads before the words.
 * The step's kind sits in a narrow mono column, the way a log reads; everything an
 * admin compares (tokens, cost, the model) sits in the facts grid under the steps. */
.trace-pick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin: 0 0 var(--space-xl);
  font-size: var(--font-size-sm);
}
.trace-pick label { font-weight: 600; color: var(--color-text-secondary); }
.trace-pick input { width: 160px; max-width: 100%; }
.trace-count {
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.trace-muted { color: var(--color-text-muted); }
.trace-turns { list-style: none; margin: 0; padding: 0; }
.trace-turns > li { margin: 0 0 var(--space-md); }
details.trace-turn {
  margin: 0;
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  scroll-margin-top: var(--space-lg);
}
details.trace-turn[open] { border-color: var(--color-ledger-rail); }
details.trace-turn > summary.trace-turn-head {
  display: grid;
  /* The chevron is the summary's ::before, so it takes the first column. */
  grid-template-columns: 1ch max-content minmax(0, 1fr) max-content;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background: none;
  border-radius: var(--radius-md);
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--font-size-sm);
  font-weight: 400;
  color: var(--color-text-primary);
}
details.trace-turn > summary.trace-turn-head:hover { background: var(--color-bg-hover); }
details.trace-turn > summary.trace-turn-head::before {
  content: "\203A";
  display: inline-block;
  font-size: var(--font-size-md);
  line-height: 1;
  color: var(--color-text-muted);
  transition: transform 0.12s ease;
}
/* The shared open marker swaps the glyph for a triangle; keep the chevron, turned. */
details.trace-turn[open] > summary.trace-turn-head::before { content: "\203A"; transform: rotate(90deg); }
details.trace-turn > summary .trace-turn-when {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
details.trace-turn > summary .trace-turn-ask {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
details.trace-turn > summary .trace-turn-meta {
  display: flex;
  gap: var(--space-md);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.trace-steps {
  list-style: none;
  margin: var(--space-xs) var(--space-lg) 0 calc(var(--space-lg) + var(--space-sm));
  padding: 0 0 0 var(--space-lg);
  border-left: 1px solid var(--color-ledger-rail);
}
.trace-step {
  position: relative;
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: var(--space-md);
  padding: var(--space-sm) 0;
}
.trace-step-tool::before,
.trace-step-gate::before,
.trace-step-approval::before,
.trace-step-write::before {
  content: "";
  position: absolute;
  left: calc(var(--space-lg) * -1);
  top: calc(var(--space-sm) + 0.7em);
  width: var(--space-sm);
  height: 2px;
  background: var(--color-ledger-tick);
}
.trace-step-kind {
  padding-top: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
.trace-step-body { min-width: 0; font-size: var(--font-size-sm); line-height: 1.5; }
.trace-step-body p { margin: 0; }
.trace-step-body p + p { margin-top: 2px; }
.trace-step-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-xs) var(--space-sm); }
.trace-step-title code, .trace-io code {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  overflow-wrap: anywhere;
}
.trace-step-text { max-width: var(--measure-prose); overflow-wrap: anywhere; }
.trace-step-message .trace-step-text { font-weight: 500; }
.trace-agent-text { color: var(--color-text-secondary); }
/* A step placed from the audit log by person and time (IW-782 review): said in the
 * quietest register on the line, so it qualifies the step without competing with it. */
.trace-matched {
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--color-text-muted);
}
.trace-matched-note { margin: 0 0 var(--space-md); }
/* How long a tool call took (IW-784): the figure ends the title line, and a hairline
 * under it runs the call's share of the turn, so the step that held a slow turn is
 * the one with the long line. A call that took half the turn or more is said in
 * the primary register and its line takes the rail's tick colour; the rest stay
 * muted on the rail's own grey. */
.trace-took {
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}
.trace-took.trace-took-most { color: var(--color-text-primary); font-weight: 600; }
.trace-took-rule {
  display: block;
  /* The width is the call's share, set on the element by the template. */
  min-width: 2px;
  height: 2px;
  margin: 3px 0 var(--space-xs);
  background: var(--color-ledger-rail);
}
.trace-took-rule.trace-took-most { background: var(--color-ledger-tick); }
.trace-io {
  display: grid;
  grid-template-columns: 3ch minmax(0, 1fr);
  gap: var(--space-sm);
  color: var(--color-text-secondary);
}
.trace-io-label { font-size: 11px; color: var(--color-text-muted); }
.trace-step-end .trace-step-text { font-size: var(--font-size-xs); }
.trace-cost {
  max-width: none;
  margin: var(--space-md) var(--space-lg) 0;
  padding: var(--space-md) 0 var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
}
.trace-cost code { font-family: var(--font-mono); font-size: var(--font-size-xs); }
.trace-turn-link { color: var(--color-text-secondary); }
@container pane (max-width: 560px) {
  details.trace-turn > summary.trace-turn-head {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-md);
  }
  details.trace-turn > summary.trace-turn-head::before { display: none; }
  details.trace-turn > summary .trace-turn-ask { white-space: normal; }
  .trace-steps { margin: var(--space-xs) var(--space-md) 0 var(--space-md); padding-left: var(--space-md); }
  .trace-step { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .trace-step-tool::before, .trace-step-gate::before,
  .trace-step-approval::before, .trace-step-write::before { left: calc(var(--space-md) * -1); }
  .trace-cost { margin: var(--space-md) var(--space-md) 0; }
}
@media (prefers-reduced-motion: reduce) {
  details.trace-turn > summary.trace-turn-head::before { transition: none; }
}
.admin-subhead {
  margin: var(--space-xl) 0 var(--space-xs);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-primary);
}
.admin-anchor { display: block; }

/* ── Sign-in (IW-623) ──
 * The state line is the answer the panel gives: on or off, and from where. A rule on
 * its leading edge carries the state in colour, and the words carry it without. */
.admin-sign-in-state {
  margin: 0 0 var(--space-lg);
  max-width: var(--measure-prose);
  padding: var(--space-xs) 0 var(--space-xs) var(--space-md);
  border-left: 3px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  font-weight: 500;
  line-height: 1.5;
  color: var(--color-text-primary);
}
.admin-sign-in-state:empty { display: none; }
.admin-sign-in-state[data-state="stored"],
.admin-sign-in-state[data-state="env"] { border-left-color: var(--color-status-resolved); }

/* ── A connection's Test (IW-629) ──
 * One strip per connection, at the head of the panel holding its settings: the name
 * and its lamp on the left, Test and the guide link on the right, and under a failure
 * the line saying what to do. The lamp is the Connection health table's, so a result
 * reads the same in both places. Two strips in one panel stack as a pair. */
.admin-conn-check {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-xs) var(--space-lg);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  max-width: var(--measure-wide);
}
.admin-conn-checks {
  display: grid;
  gap: var(--space-sm);
  margin: 0 0 var(--space-lg);
}
.admin-conn-check p { margin: 0; }
.admin-conn-check-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  min-width: 0;
}
.admin-conn-check-name {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-primary);
}
.admin-conn-check-actions {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}
.admin-conn-check-actions a {
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.admin-conn-check-actions a:hover { color: var(--color-text-primary); }
.admin-conn-check-detail { grid-column: 1 / -1; max-width: var(--measure-prose); }
.admin-conn-check-detail[hidden] { display: none; }
#admin-health-table .admin-conn-check-detail { max-width: 48ch; }
/* Test and Setup guide each read as one control, so neither breaks across lines. */
#admin-health-table td:nth-child(n+3) { white-space: nowrap; }
/* On a phone the four health columns cannot share the width, so each row stacks the
 * way My settings' credentials do: the name with Test and Setup guide on one line,
 * the status and what to do under it at full width. */
@container pane (max-width: 560px) {
  .admin-conn-check { grid-template-columns: minmax(0, 1fr); }
  #admin-health-table thead { display: none; }
  #admin-health-table,
  #admin-health-table tbody { display: block; }
  #admin-health-table tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: baseline;
    gap: var(--space-xs) var(--space-md);
    padding: var(--space-md) 0;
    border-top: 1px solid var(--color-border-subtle);
  }
  #admin-health-table td { padding: 0; border-top: none; }
  #admin-health-table td:nth-child(2) { grid-row: 2; grid-column: 1 / -1; text-align: left; }
}

/* The setup steps are a real sequence done in another tab, so each keeps its number
 * in the accent, set apart in its own column so a person returning from Atlassian
 * finds their place by number at a glance. */
.admin-steps {
  counter-reset: admin-step;
  list-style: none;
  margin: 0 0 var(--space-lg);
  padding: 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.55;
  color: var(--color-text-primary);
}
/* A plain disclosure, not the brief's boxed card header the global details rule draws:
 * the steps sit in the panel's own flow, and the summary reads as a sentence. */
.admin-steps-fold {
  margin: 0 0 var(--space-lg);
  border: none;
  border-radius: 0;
}
.admin-steps-fold[hidden] { display: none; }
.admin-steps-fold > summary {
  margin-bottom: var(--space-md);
  padding: 0;
  background: none;
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-primary);
}
.admin-steps-fold:not([open]) > summary { margin-bottom: 0; }
.admin-steps-fold .admin-steps { margin-bottom: 0; }
#admin-sign-in .admin-note a { color: var(--color-accent); }
#admin-user-search-note:not(:empty) { margin-bottom: var(--space-md); }
.admin-steps li {
  counter-increment: admin-step;
  position: relative;
  padding: 0 0 var(--space-md) 36px;
}
.admin-steps li::before {
  content: counter(admin-step);
  position: absolute;
  left: 0;
  top: 0;
  width: 24px;
  height: 24px;
  border: 1.5px solid var(--color-accent);
  border-radius: 50%;
  color: var(--color-accent);
  font-size: var(--font-size-xs);
  font-weight: 600;
  line-height: 21px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.admin-steps a { color: var(--color-accent); }
/* The callback address: an exact string to paste elsewhere, so it is set in the
 * mono on the code ground, whole and selectable, with its Copy button beside it. */
.admin-copy-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}
.admin-copy-line code {
  padding: 5px var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-primary);
  word-break: break-all;
  user-select: all;
}
.admin-copy-line .btn-secondary { padding: 4px var(--space-md); }
.admin-form[hidden], #admin-sign-in-form[hidden] { display: none; }
#admin-sign-in-facts { margin-top: var(--space-lg); }

/* ── Microsoft 365 (IW-628) ──
 * The Sign-in panel's pattern: state line, numbered steps, form, facts. The one new
 * piece is the permission list inside step 3: three exact names typed into Entra's
 * search one at a time, so each is its own copy line with what it is for beside it.
 * Not numbered: the order they are added in does not matter. */
#admin-m365 .admin-note a { color: var(--color-accent); }
/* Alert email (IW-643): the same guide note, the same link colour. */
#admin-alerts .admin-note a { color: var(--color-accent); }
/* Single-seat note (IW-646): sits between the heading row and the intro, with
 * room above it from .admin-note and below it before the intro. Enroll user takes
 * the disabled look but stays clickable, because the click explains why. */
#admin-single-seat-note { margin: 0 0 var(--space-lg); }
#admin-single-seat-note a { color: var(--color-accent); }
#admin-enroll-btn[aria-disabled="true"] {
  background: var(--color-border-subtle);
  color: var(--color-text-muted);
  cursor: not-allowed;
  filter: none;
}
#admin-m365-facts { margin-top: var(--space-lg); }
/* One grid for the three rows, so the Copy buttons and what each is for line up
 * whatever the length of the name before them. */
.admin-steps .admin-m365-perms {
  display: grid;
  grid-template-columns: max-content max-content minmax(0, 1fr);
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  list-style: none;
  margin: var(--space-sm) 0 0;
  padding: 0;
}
.admin-steps .admin-m365-perms li,
.admin-m365-perms .admin-copy-line { display: contents; }
.admin-steps .admin-m365-perms li::before { content: none; }
.admin-m365-perms .admin-m365-use {
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
}
/* A tight pane has no room for three columns: the name and its button keep a row,
 * and what it is for drops under them. The pane's tight step (IW-391). */
@container pane (max-width: 560px) {
  .admin-steps .admin-m365-perms { grid-template-columns: max-content max-content; }
  .admin-m365-perms .admin-m365-use { grid-column: 1 / -1; margin-bottom: var(--space-xs); }
}
/* The sign-in method choice: two options side by side, reading as one question. */
.admin-choice {
  border: none;
  margin: 0 0 var(--space-md);
  padding: 0;
}
.admin-choice legend {
  margin-bottom: var(--space-xs);
  padding: 0;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.admin-choice label,
.admin-check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  margin: 0 var(--space-lg) 0 0;
  font-size: var(--font-size-sm);
  font-weight: 400;
  color: var(--color-text-primary);
  cursor: pointer;
}
.admin-choice label:has(input:disabled) { color: var(--color-text-muted); cursor: default; }
.admin-check { margin: 0 0 var(--space-sm); }
/* People found by name: each a full-width row to press, the name over the email
 * that tells two people with one name apart. */
.admin-people {
  list-style: none;
  margin: 0 0 var(--space-sm);
  padding: 0;
  max-width: 380px;
}
.admin-people:empty { display: none; }
.admin-person {
  display: block;
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-top: none;
  background: var(--color-bg-surface);
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.admin-people li:first-child .admin-person {
  border-top: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.admin-people li:last-child .admin-person { border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
.admin-people li:only-child .admin-person { border-radius: var(--radius-sm); }
.admin-person:hover, .admin-person:focus-visible { background: var(--color-bg-hover); }
.admin-person-name {
  display: block;
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
}
.admin-person-detail {
  display: block;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  word-break: break-word;
}
.admin-picked {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
}
.admin-picked[hidden] { display: none; }
.admin-picked .admin-person-detail { display: inline; }
.admin-form-title {
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-primary);
}

/* Docs suggestions waiting from before IW-870 (IW-480). Each is a passage of prose,
 * not a row of values, so the list reads like the settings list above rather than
 * a table: hairlines between entries, the page as the entry's heading, the proposed
 * text folded away, and under it the headline its IKB card will carry beside File
 * in IKB. A filed entry leaves; the list and its heading go once none wait. */
.docs-sugg-leftovers[hidden] { display: none; }
/* Filing switched off: a sentence that says where to turn it on, above the list. */
.docs-sugg-off {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.docs-sugg-off[hidden] { display: none; }
/* The filing's result names the IKB card it became; its key is a link in the alert's tone. */
#admin-docs-suggestions-message a { color: inherit; font-weight: 600; }
.docs-sugg-empty {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.docs-sugg {
  padding: var(--space-md) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.docs-sugg-title {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
.docs-sugg-title a { color: inherit; }
.docs-sugg-where {
  margin: 2px 0 0;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}
.docs-sugg-meta {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.docs-sugg-problem {
  margin: var(--space-sm) 0 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-primary);
  white-space: pre-wrap;
}
/* Not the brief's section-summary register (details > summary): a quiet fold in
 * sentence case, the way this page writes its headings. .admin-fold is the same
 * fold anywhere else on the console (the library table's start pages, IW-483). */
.docs-sugg-change, .admin-fold { margin: var(--space-sm) 0 0; border: none; border-radius: 0; }
.docs-sugg-change > summary, .admin-fold > summary {
  display: inline-block;
  padding: 0;
  background: none;
  border-radius: 0;
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.docs-sugg-change > summary:hover, .admin-fold > summary:hover { color: var(--color-text-primary); }
/* Inside a table cell the fold sits on the row's baseline, with no margin above. */
.admin-table .admin-fold { margin: 0; }
.docs-sugg-source {
  margin: var(--space-xs) 0 0;
  padding: var(--space-sm) var(--space-md);
  max-height: 22rem;
  overflow: auto;
  border-radius: var(--radius-sm);
  background: var(--color-bg-code);
  color: var(--color-text-primary);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.docs-sugg-file {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-md);
}
.docs-sugg-headline {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: 1 1 18rem;
  max-width: 34rem;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.docs-sugg-headline input {
  box-sizing: border-box;
  min-height: var(--control-height);
  padding: 5px 8px;
  font-size: var(--font-size-sm);
  font-weight: 400;
}
.docs-sugg-file .btn-secondary { min-height: var(--control-height); }
/* Tables keep their columns and scroll inside their panel on a narrow screen, so
 * the page itself never scrolls sideways. */
.admin-table-wrap { max-width: 100%; overflow-x: auto; }
/* A count column: figures right-aligned on tabular digits, so two rows compare
 * down the column at a glance. Ties the last-column rule and follows it. */
.admin-table th.admin-num, .admin-table td.admin-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The library table (IW-483). A site is one cell: its host, the maker it is filed
 * under beneath in the quiet register, and its start pages as a count that opens
 * to the list. The last crawl is one cell too: its time, what it left in words
 * (so "partial" and "queued" survive a glance and greyscale), and any problem it
 * reported beneath, the one text that can run long, wrapped within a bounded
 * width with the whole of it as the title. Every other cell keeps to one line, so
 * on a narrow screen the table scrolls inside its panel rather than folding its
 * columns into stacks of single words. */
.partner-sites th, .partner-gaps th, .partner-gaps td { white-space: nowrap; }
.partner-sites td { vertical-align: top; }
.partner-site-when, .partner-site-state { display: block; white-space: nowrap; }
/* A host wraps rather than stretching the Site column: a very long one was
 * pushing Rebuild now off a 1440 wide screen with nothing to say the table
 * scrolled (IW-534). A host has no spaces, so it breaks anywhere, and the cap
 * keeps the column to a measure a short host never reaches. */
.partner-site-host {
  display: block;
  max-width: 18rem;
  overflow-wrap: anywhere;
}
.partner-site-maker, .partner-site-state {
  display: block;
  margin-top: 2px;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* The crawl column is bounded by a comfortable measure and otherwise flexes, so
 * an open start page list can take width from it and Rebuild now stays on the
 * screen; its floor for a phone is set at the tight step below (IW-487). */
.partner-site-crawl { max-width: 20rem; }
/* A problem wraps at its spaces and after the slashes of an address, which the
 * renderer marks with <wbr>; "anywhere" is only the fallback for a single part
 * longer than the column, so it can never widen the table. */
.partner-site-problem {
  display: block;
  margin-top: var(--space-xs);
  font-size: var(--font-size-xs);
  line-height: 1.45;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
/* Show all / Show less under a problem: drawn for every problem, shown by the
 * tight step below, and only on a cell the renderer has marked clamped. */
.partner-site-more {
  display: none;
  margin-top: 2px;
  font-size: var(--font-size-xs);
}
/* At the pane's tight step the table scrolls sideways and the crawl column sits
 * off screen, so a long problem there was only a tall blank under the site name.
 * Three lines is enough to name the file and the reason; the button opens the
 * rest in place, and the title still carries the whole text. Once any row
 * carries a problem the column also gets the width an address needs to read
 * whole on a line or two, since here the table scrolls anyway; without the
 * floor it shrinks to its state words and a problem wraps to a word a line. A
 * table with no problem keeps its narrower column and shorter scroll. */
@container pane (max-width: 560px) {
  .partner-site-crawl:has(.partner-site-problem) { min-width: 16rem; }
  .partner-site-problem {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
  }
  .partner-site-problem.is-open { -webkit-line-clamp: none; }
  .partner-site-crawl.is-clamped .partner-site-more { display: inline-block; }
}
.partner-site .admin-fold { margin-top: var(--space-xs); }
.partner-site .admin-fold > summary { white-space: nowrap; font-weight: 500; }
/* Open, the list asks for the width a whole address needs and wraps the rest. */
.partner-site-pages {
  min-width: 16rem;
  margin: var(--space-xs) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--font-size-xs);
}
.partner-site-pages li { margin: 0 0 2px; overflow-wrap: anywhere; }
.partner-sites td:last-child .btn-link { white-space: nowrap; }
/* The library table scrolls inside its panel when the pane is narrow, and on a
 * phone only the Site column shows (IW-487 accepted that; IW-534 adds the cue). A
 * soft edge on each side that still has more to scroll to: the shadows are fixed
 * to the box and the panel-coloured covers scroll with the content, so each shadow
 * is covered while its edge is the table's own and shows only when there is more
 * beyond it. No script, and no cue where the table fits. */
#admin-partner-sites {
  background:
    linear-gradient(to right, var(--color-bg-surface) 30%, transparent) left center / 2.5rem 100% no-repeat local,
    linear-gradient(to left, var(--color-bg-surface) 30%, transparent) right center / 2.5rem 100% no-repeat local,
    linear-gradient(to right, color-mix(in srgb, var(--color-text-primary) 12%, transparent), transparent) left center / 1rem 100% no-repeat scroll,
    linear-gradient(to left, color-mix(in srgb, var(--color-text-primary) 12%, transparent), transparent) right center / 1rem 100% no-repeat scroll;
}
.partner-gaps td.partner-gap-topic { white-space: normal; min-width: 12rem; }
/* Dates and months keep to one line and scroll with their table, rather than
 * breaking at the hyphen on a phone. */
#admin-sessions td:nth-child(2), #admin-sessions td:nth-child(3),
#admin-spend td:first-child, #admin-open-metrics td:nth-child(3) { white-space: nowrap; }

/* One setting: its name with the key beside it in small mono for support, what it
 * does, when a change applies and what is in force, and the control on the right. */
.admin-settings-list { margin-top: var(--space-md); }
.admin-setting {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-xs) var(--space-xl);
  padding: var(--space-md) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.admin-setting[hidden] { display: none; }
.admin-setting-text { min-width: 0; max-width: var(--measure-prose); }
.admin-setting-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-sm);
  margin: 0;
}
.admin-setting-label {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-primary);
}
.admin-setting-key {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.02em;
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}
.admin-setting-help {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.admin-setting-help a,
.admin-panel-intro a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.admin-setting-meta {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
/* The running version at the foot of Connection health (IW-739): the same quiet
 * meta line, a step clear of the table above it. */
.admin-version { margin-top: var(--space-sm); }
/* The version opens What's new (IW-741). It stays in the meta line's own colour, so
 * the line reads as quietly as before; the underline says it can be followed. */
.admin-version a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.admin-version a:hover { color: var(--color-text-secondary); }
.admin-version a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* The one apply mode that asks something of the admin reads in the warning colour,
 * in words as well, so it survives greyscale. */
.admin-setting-restart { font-weight: 600; color: var(--color-status-warning); }
.admin-setting-control { align-self: start; padding-top: 2px; }
/* A model id is long; its select is wide enough to show one whole. */
.admin-page .admin-setting-form select { width: 250px; flex-basis: 250px; }
.admin-page .admin-table a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.admin-setting-msg {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-status-resolved);
}
.admin-setting-msg[hidden] { display: none; }
.admin-setting-msg.admin-setting-msg-error { color: var(--color-status-fault); }

/* Narrow pane: the control drops under its explanation, and panels give back some
 * padding so a phone keeps its width for the text. */
@container pane (max-width: 720px) {
  .admin-page .admin-section { padding: var(--space-md) var(--space-lg) var(--space-lg); }
  .admin-setting { grid-template-columns: minmax(0, 1fr); }
  .admin-setting-control { padding-top: var(--space-xs); }
  .admin-page .admin-setting-form { justify-content: flex-start; }
  .admin-page .admin-setting-form input,
  .admin-page .admin-setting-form select { flex: 1 1 160px; width: auto; }
}

/* ── Shared row states (from the candidate review, IW-108) ────────────────────
 * The store-fault notice and the erase control, read by the Boat names queue
 * and the Teams channels page. Erase is the product's one irreversible
 * action, so it reads as a fault-colored link at rest and the confirm step
 * gets weight. */
.cr-notice {
  color: var(--color-status-fault);
  padding: var(--space-md) 0;
  font-size: var(--font-size-sm);
}
/* .btn-link is colored for the inverse header; these sit on a card. */
.cr-danger { color: var(--color-status-fault); opacity: 1; }
.cr-danger:hover { text-decoration: underline; }
.cr-confirm { font-weight: 600; }

/* ── Boats page (IW-249) ─────────────────────────────────────────
 * The shell, lookup, result table, and boat page. The registry cards keep
 * their own .rq-* section above, because the moved script selects on those
 * names. The boat page's queue sits inside this shell, so its own max-width
 * and padding are released here. */
.registry-page { max-width: 960px; margin: 0 auto; padding: var(--space-lg) 0; }
.registry-page .registry-queue { max-width: none; padding: var(--space-md) 0 0; }
.reg-header { display: flex; align-items: baseline; gap: var(--space-md); }
.reg-title { font-size: 1.25rem; margin: 0; }
/* A boat's key beside its name (IW-568): there to quote, so it never breaks at its hyphen. */
.reg-key { font-family: var(--font-mono); font-size: var(--font-size-sm); color: var(--color-text-muted); font-weight: 400; white-space: nowrap; }
.reg-intro, .reg-fleet, .reg-meta {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.reg-intro { margin: var(--space-xs) 0 var(--space-md); max-width: var(--measure-wide); }
/* What the registry is, behind a disclosure (IW-356). The summary keeps the
 * product's disclosure register and marker, from the details rules near the
 * top of this file, and drops the box: closed, it is one quiet line under the
 * title and not a panel above the tabs. The body is running text, so it takes
 * the prose measure. It was set at --measure-wide, which is for tables and
 * code, and ran well past a hundred characters a line at this size. */
.reg-about { border: 0; margin: var(--space-xs) 0 var(--space-sm); }
.reg-about > summary {
  display: inline-block;
  padding: 0;
  background: none;
  color: var(--color-text-muted);
}
.reg-about > summary:hover { color: var(--color-text-primary); }
.reg-about-body {
  max-width: var(--measure-prose);
  padding: var(--space-xs) 0 var(--space-sm);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}
.reg-about-body p { margin: 0 0 var(--space-sm); }
.reg-about-body p:last-child { margin-bottom: 0; }
/* Where the data comes from (IW-565). The one rule the whole panel serves is
 * the only line set in the text colour and a weight, so it is what an open
 * panel says first. The six sources are a classification, not steps, so they
 * carry no numbers: each is its own group, parted from the last by a hairline,
 * with its name as the lead in the text colour, one muted line on how it
 * arrives, and then what it writes. One column at every width, inside the
 * prose measure, so a phone reads it the way a desktop does. */
.reg-about-body .reg-about-rule {
  margin: var(--space-md) 0;
  color: var(--color-text-primary);
  font-size: var(--font-size-md);
  font-weight: 600;
}
.reg-about-sources,
.reg-about-terms { margin: 0 0 var(--space-md); }
.reg-about-source {
  padding: var(--space-md) 0 var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.reg-about-source dt {
  color: var(--color-text-primary);
  font-size: var(--font-size-md);
  font-weight: 600;
}
.reg-about-source dd { margin: 2px 0 0; }
.reg-about-body .reg-about-how { margin: 0 0 var(--space-xs); color: var(--color-text-muted); }
/* The Learnings link takes the page's own link register, Show all's, so it
 * reads as a link in both themes and not as the browser's default blue. */
.reg-about-body a {
  color: var(--color-text-primary);
  text-decoration: underline;
  text-decoration-color: var(--color-text-muted);
  text-underline-offset: 2px;
}
/* Every other link on a Boats page that no rule of its own colours takes the
 * same register (IW-800), so none is left in the browser's blue or visited
 * purple: the Admin settings link in the Teams notice, a partner company's back
 * link, a boat's ticket links. :where() keeps it at zero specificity, so a link
 * styled on purpose (a fleet row, a partner name, evidence) keeps its own look.
 * Facts about boats is a Boats queue drawn by the Learnings page's template,
 * so that queue is named and the Learnings page is not. */
:where(.registry-page, .teams-review, .learnings-page[data-queue="facts"]) a {
  color: var(--color-text-primary);
  text-decoration: underline;
  text-decoration-color: var(--color-text-muted);
  text-underline-offset: 2px;
}
.reg-about-source ul { margin: 0; padding-left: 1.25em; }
.reg-about-source li + li { margin-top: var(--space-xs); }
/* The two words defined, set as a glossary under the sources: the term in the
 * text colour and a weight, its sentence running on beside it. */
.reg-about-terms {
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
}
.reg-about-term + .reg-about-term { margin-top: var(--space-sm); }
.reg-about-term dt,
.reg-about-term dd { display: inline; margin: 0; }
.reg-about-term dt { color: var(--color-text-primary); font-weight: 600; }
.reg-about-term dt::after { content: ". "; }
/* The same measure for the rest of the page's running text. The boat page (.reg-boat-page) shares
 * the .registry-page shell and is left out, and the Learnings page shares
 * .reg-intro: neither is this card's to restyle. */
.registry-page:not(.reg-boat-page) .reg-empty,
.registry-page:not(.reg-boat-page) .reg-notice,
.registry-page:not(.reg-boat-page) .reg-meta,
.registry-page:not(.reg-boat-page) .rq-pii-note,
.registry-page:not(.reg-boat-page) .rq-empty { max-width: var(--measure-prose); }
/* The fleet count (IW-356) reads as data: the figures take the text colour
 * and a weight, at the size of the line they sit in, and the words stay muted.
 * Tabular figures so a count that changes does not shift the words after it. */
.reg-fleet { margin: var(--space-md) 0 var(--space-sm); }
.reg-fleet-n {
  color: var(--color-text-primary);
  font-size: var(--font-size-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* The identifier's own sentence inside a miss: a line of its own under the
 * state's message, so the message still reads first. */
.reg-teach { display: block; margin-top: var(--space-xs); }
/* The Boats tab lookup (IW-291, IW-353). Two lines. Line one is mode, value,
 * action, in the order the operator works. Line two holds what widens the
 * lookup and the Show all link, so nothing optional sits between the value
 * and the button, and the button is the row's only accent. */
.reg-lookup { margin: var(--space-md) 0 var(--space-sm); }
.reg-lookup-row,
.reg-lookup-more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}
.reg-lookup-row { gap: var(--space-sm); }
.reg-lookup-more { gap: var(--space-md); margin-top: var(--space-sm); }
.reg-lookup-by,
.reg-lookup-parked {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
/* Every select in the product is as wide as its container, which here is an
 * inline flex box beside a label that is free to shrink: the select took the
 * width and the label collapsed to its longest word. This one select sizes to
 * its options instead, and the label never breaks. */
.reg-lookup-by { flex: none; }
.reg-lookup-by label { white-space: nowrap; }
.reg-lookup-by select { width: auto; }
.reg-lookup-by select,
.reg-lookup-q,
.reg-lookup-row .btn-primary {
  height: var(--control-height);
  box-sizing: border-box;
}
.reg-lookup-field { position: relative; display: flex; flex: 1 1 240px; min-width: 0; }
.reg-lookup-field > .reg-lookup-q { flex: 1 1 auto; min-width: 0; }
.reg-lookup-row .btn-primary { display: inline-flex; align-items: center; }
/* What "parked" means, said beside the checkbox that asks about it (IW-565).
 * Muted and a step smaller so the checkbox still reads first. The two are one
 * group with a tighter gap than the row's, so they read as one control and
 * Show all stays apart; on a phone the hint wraps to a line of its own under
 * the checkbox, inside the column rather than past its edge. */
.reg-lookup-parked-group {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-sm);
}
.reg-lookup-hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* Lookup suggestions (IW-320, IW-355). The containing block is the field, the
 * box that holds the input and nothing else, so the list takes the input's own
 * left edge and width and floats over what is below. It was the whole of line
 * one, which ran the list from the Find by label to the button and, once a
 * narrow window wrapped the row, opened it under the second line of controls
 * and away from the input. The field is a flex box so the input fills it with
 * no inline gap under it, and it carries the flex sizing the input used to.
 * The gap under the input is the one .rail-pop leaves, and it also clears the
 * input's focus outline. */
.reg-suggest {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: var(--space-xs);
  z-index: 20;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
  overflow: hidden;
}
.reg-suggest-list { list-style: none; margin: 0; padding: var(--space-xs) 0; max-height: 20rem; overflow-y: auto; }
.reg-suggest-item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-md);
  cursor: pointer;
}
/* Hover and the keyboard highlight are two states and look like two (IW-355).
 * Return opens the highlighted boat and runs the search when nothing is
 * highlighted, so the operator has to see which one they are in. Hover is the
 * fill alone. The highlight is the ticket rail's selected row, the fill plus
 * the accent edge, and it is that rule itself: .reg-suggest-active is grouped
 * into .rail-entry-active above, so the two cannot drift apart. */
.reg-suggest-item:hover { background: var(--color-bg-hover); }
.reg-suggest-name { font-weight: 600; }
.reg-suggest-meta { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.reg-suggest-tag {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-status-warning);
}
.reg-suggest-note {
  padding: var(--space-xs) var(--space-md);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.reg-empty { color: var(--color-text-muted); padding: var(--space-md) 0; }
.reg-notice { color: var(--color-status-fault); padding: var(--space-md) 0; font-size: var(--font-size-sm); }
.reg-parked-mark {
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  color: var(--color-status-warning);
}
.reg-person { margin: var(--space-md) 0; }
.reg-person-name { font-size: var(--font-size-md); margin: 0 0 var(--space-xs); }
.reg-back { font-size: var(--font-size-sm); margin: 0 0 var(--space-sm); }
.reg-parked-banner {
  background: var(--color-status-warning-bg);
  color: var(--color-text-primary);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  margin: var(--space-sm) 0;
  font-size: var(--font-size-sm);
}
.reg-section { margin: var(--space-lg) 0; }
.reg-section-title {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-sm);
}
.reg-fields { display: grid; grid-template-columns: 12rem 1fr; gap: var(--space-xs) var(--space-md); margin: 0; font-size: var(--font-size-sm); }
.reg-fields dt { color: var(--color-text-secondary); }
.reg-fields dd { margin: 0; }
.reg-history summary { cursor: pointer; color: var(--color-text-muted); font-size: var(--font-size-xs); }
.reg-history ul, .reg-list { margin: var(--space-xs) 0 0; padding-left: var(--space-md); }

/* ── Integrel partner companies (IW-747) ───────────────────────────────────
 * A boat's partners on its page, and a partner company's own page. One mark
 * carries the meaning: a partner looking after the boat now stands on the
 * accent rule, an earlier one on the hairline in the muted tone, so current
 * and earlier read apart before a date is read. A company's locations,
 * contacts and addresses use the same list, each with its own remove. */
.reg-partners { margin: var(--space-sm) 0; }
.reg-partners-title,
.reg-partners-earlier-title {
  font-size: var(--font-size-sm);
  font-weight: 600;
  margin: var(--space-md) 0 var(--space-xs);
}
.reg-partners-earlier-title { color: var(--color-text-secondary); }
.reg-partner-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-sm);
  max-width: var(--measure-wide);
}
.reg-partner {
  padding: 2px 0 2px var(--space-sm);
  border-left: 3px solid var(--color-accent);
  font-size: var(--font-size-sm);
}
.reg-partner-earlier {
  border-left: 1px solid var(--color-border-subtle);
  padding-left: calc(var(--space-sm) + 2px);
  color: var(--color-text-secondary);
}
.reg-partner-line { font-size: var(--font-size-md); line-height: 1.35; }
.reg-partner-earlier .reg-partner-name { font-weight: 500; }
.reg-partner-name { font-weight: 600; color: var(--color-text-primary); }
a.reg-partner-name { text-decoration: none; }
a.reg-partner-name:hover,
a.reg-partner-name:focus-visible { text-decoration: underline; }
.reg-partner-place { color: var(--color-text-secondary); }
.reg-partner .reg-meta { margin: 2px 0 0; }
.reg-partner-flag {
  display: inline-block;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-sm);
  background: var(--color-status-warning-bg);
  color: var(--color-text-primary);
  vertical-align: 1px;
}
/* A partner's own remove or end control: a quiet line under it, not a form
 * box, so the list reads as a list. At .reg-form's specificity, since the
 * form's own box is declared further down the sheet. */
.reg-form.reg-partner-end {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
  padding: 0;
  border: 0;
  background: none;
}
.reg-form.reg-partner-end button { width: auto; flex: none; }
.reg-partner-add { margin-top: var(--space-md); }
.reg-partner-unconfirmed {
  background: var(--color-status-warning-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  margin: var(--space-sm) 0;
  font-size: var(--font-size-sm);
  max-width: var(--measure-wide);
}
.reg-partner-unconfirmed p { margin: 0 0 var(--space-sm); }
/* The Partner companies page (IW-964), a pilot book: every partner by the
 * sailing region it works in, a heavy rule under each region, then each
 * country with its flag on the hairline, and the companies there in columns
 * that fold to one on a phone. A name not yet confirmed carries the waiting
 * tone, since it waits on a person. */
.pidx { margin-top: var(--space-md); }
.pidx [hidden],
.pidx a.pidx-jump[hidden] { display: none; }
.pidx-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs) var(--space-md); }
.pidx input.pidx-find { flex: 1 1 18rem; width: auto; max-width: 26rem; height: var(--control-height); font-size: var(--font-size-md); }
.pidx-none-found { margin: 0; color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.pidx-jumps { margin: var(--space-md) 0 0; display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-lg); font-size: var(--font-size-sm); }
.pidx a.pidx-jump { display: inline-flex; align-items: baseline; gap: var(--space-xs); color: var(--color-text-secondary); text-decoration: none; }
.pidx a.pidx-jump:hover,
.pidx a.pidx-jump:focus-visible { color: var(--color-text-primary); text-decoration: underline; }
.pidx-count { color: var(--color-text-muted); font-weight: 400; font-variant-numeric: tabular-nums; }
.pidx-region { margin: var(--space-xl) 0 0; scroll-margin-top: var(--space-md); }
.pidx-region-title {
  margin: 0;
  padding-bottom: var(--space-xs);
  font-size: var(--font-size-title);
  font-weight: 600;
  line-height: 1.3;
  border-bottom: 2px solid var(--color-text-primary);
}
/* The count stays beside the last word when a long region name wraps. */
.pidx-region-title .pidx-count { margin-left: 2px; font-size: var(--font-size-md); }
.pidx-country { margin: var(--space-md) 0 0; }
.pidx-country-title {
  margin: 0;
  padding-bottom: var(--space-xs);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-secondary);
  border-bottom: 1px solid var(--color-border-subtle);
}
.pidx-country-title .pidx-count { font-size: var(--font-size-sm); }
/* The flag is the boat header's, at its size and on its hairline. */
.pidx-flag { flex: none; width: 16px; height: 12px; border-radius: 1px; box-shadow: 0 0 0 1px var(--color-border-subtle); }
.pidx-note { margin: var(--space-xs) 0 0; max-width: 68ch; color: var(--color-text-muted); font-size: var(--font-size-sm); }
.pidx-entries {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: 0 var(--space-xl);
}
.pidx-entry { padding: var(--space-md) 0; border-bottom: 1px solid var(--color-border-subtle); min-width: 0; }
.pidx-name { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 2px var(--space-xs); font-size: var(--font-size-prose); font-weight: 600; line-height: 1.35; }
.pidx a.pidx-link { color: var(--color-text-primary); text-decoration: none; overflow-wrap: anywhere; }
.pidx a.pidx-link:hover,
.pidx a.pidx-link:focus-visible { text-decoration: underline; }
.pidx-tag {
  display: inline-block;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-sm);
  background: var(--color-status-waiting-bg);
  color: var(--color-status-waiting);
  font-size: var(--font-size-xs);
  font-weight: 600;
  line-height: 1.6;
}
.pidx-what,
.pidx-more { margin: 2px 0 0; font-size: var(--font-size-sm); overflow-wrap: anywhere; }
.pidx-what { color: var(--color-text-secondary); }
.pidx-more { color: var(--color-text-muted); }
/* Partners that still need a name (IW-873): the Partner companies page gathers
 * the companies Integra knows only by a personal email address at the top, as
 * a short to do list rather than more of the directory. Each row is the stand
 * in name and its address on the left and an open name box on the right, on
 * the page's hairline, so naming them is typing down a column. The name box
 * drops the form's own box, since the row is already the frame; the waiting
 * tone stays on the tag, as the page's other waiting names carry it. On a
 * phone the box folds under the name. */
.pidx-unnamed-list { list-style: none; margin: var(--space-sm) 0 0; padding: 0; max-width: var(--measure-wide); }
.pidx-unnamed-entry {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(16rem, 24rem);
  align-items: start;
  gap: var(--space-sm) var(--space-xl);
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--color-border-subtle);
}
.pidx-unnamed-who { min-width: 0; }
.reg-form.pidx-rename { padding: 0; border: 0; background: none; flex: none; gap: 2px; }
.pidx-rename-row { display: flex; gap: var(--space-xs); }
.pidx-rename-row input[type=text] { flex: 1 1 auto; min-width: 0; height: var(--control-height); }
.pidx-rename-row .btn-primary { flex: none; height: var(--control-height); padding-block: 0; }
.reg-form.pidx-rename .reg-status:empty { display: none; }
@container pane (max-width: 560px) {
  .pidx-unnamed-entry { grid-template-columns: minmax(0, 1fr); }
}
/* The same news on the company's own page, said beside the name box in the
 * waiting tone: it waits on a person, and nothing is wrong. */
.reg-partner-needs-name {
  margin: var(--space-sm) 0 0;
  padding: var(--space-sm) var(--space-md);
  max-width: var(--measure-wide);
  border-radius: var(--radius-sm);
  background: var(--color-status-waiting-bg);
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
}

/* Merging two partner companies that are one business (IW-872). The
 * confirmation is the one moment on the company page that moves a lot at
 * once, so it is the page's one framed block: the waiting wash the Confirm
 * name panel uses, since it waits on the support person, with the sentence
 * at reading size so it is read before the button is pressed. The choice
 * above it is a plain label and select, and the history reuses the partner
 * list, an undone merge on the muted hairline like an earlier partner. The
 * folded company's own page says where everything went in the same quiet
 * waiting tone as a company that needs a name. */
.reg-partner-merge-intro { max-width: var(--measure-wide); }
.reg-partner-merge-confirm {
  margin: var(--space-md) 0;
  padding: var(--space-md);
  max-width: var(--measure-wide);
  border-radius: var(--radius-sm);
  background: var(--color-status-warning-bg);
}
.reg-partner-merge-sentence {
  margin: 0;
  max-width: 68ch;
  font-size: var(--font-size-md);
  line-height: 1.5;
  color: var(--color-text-primary);
}
.reg-partner-merge-confirm .reg-form.reg-partner-end { margin-top: var(--space-md); }
/* The button names both companies, so on a phone it wraps inside the panel. */
.reg-partner-merge-confirm .reg-form.reg-partner-end button {
  max-width: 100%;
  height: auto;
  min-height: var(--control-height);
  white-space: normal;
  text-align: left;
}
.reg-partner-merge-cancel { font-size: var(--font-size-sm); }
.reg-partner-merge-choose {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-sm);
  margin: var(--space-md) 0 0;
  max-width: var(--measure-wide);
}
.reg-partner-merge-choose label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 18rem;
  min-width: 0;
  font-size: var(--font-size-sm);
}
.reg-partner-merge-choose select { width: 100%; height: var(--control-height); }
.reg-partner-merge-choose button { flex: none; height: var(--control-height); }
.reg-partner-merged-note {
  margin: var(--space-md) 0 0;
  padding: var(--space-sm) var(--space-md);
  max-width: var(--measure-wide);
  border-radius: var(--radius-sm);
  background: var(--color-status-waiting-bg);
  font-size: var(--font-size-md);
}
.reg-partner-merged-note p { margin: 0; }

/* The New boat card's new partner company (IW-747): its name, its trade under
 * a label that names it, its first location and the requester's tick, as one
 * short form under the partner row. */
.rq-company-new { display: grid; gap: var(--space-sm); max-width: 32rem; }
/* Each label sits on its own control, closer to it than to the one above. */
.rq-company-field { display: grid; gap: 2px; min-width: 0; }
.rq-company-field .cr-pick-label { margin: 0; }
.rq-company-field input { width: 100%; box-sizing: border-box; }
.rq-company-new > .rq-company-field:first-child input { max-width: 22rem; }
.rq-company-place-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-sm);
}
.rq-company-join[hidden],
.rq-company-contact[hidden] { display: none; }
@container pane (max-width: 560px) {
  .rq-company-place-row { grid-template-columns: minmax(0, 1fr); }
}

/* The About this boat panel's partner rows (IW-747): the new company's rows
 * show only under their choice. */
.boat-field[hidden] { display: none; }
.reading-partner-current { color: var(--color-text-secondary); }

/* ── The fleet board (IW-614) ──────────────────────────────────────────────
 * Every boat on the Boats tab, ranked by service activity. A fleet under
 * service rather than a spreadsheet: rows parted by the page's hairline, no
 * card per boat, no column headings. Three zones a row: who the boat is, its
 * last 24 months as a strip, and its figures with the one it ranks by first.
 *
 * Colour has two jobs and no others. The open status colour marks a boat with
 * an open ticket (the row's edge and its "open" mark) and a month of the strip
 * that still holds one; it is the board's "this one needs you". Everything else
 * is the text ramp, so the open colour is never a decoration. No token is new.
 *
 * The strip is the board's one memorable element and it carries data only: a
 * mark per month, oldest at the left, its height the tickets active that month
 * (one, two, three or more), a quiet month a hairline on the ledger rail's
 * colour, and a new year a wider gap, so a boat's rhythm reads at a glance. */
.fleet-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
}
.fleet-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-sm) var(--space-md);
}
.fleet-control {
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* Selects size to their options here, as Find by's does, so five filters sit
 * on one line at desktop width and wrap whole on a narrow one. */
.fleet-control select {
  width: auto;
  max-width: 14rem;
  height: var(--control-height);
  box-sizing: border-box;
  padding-top: 0;
  padding-bottom: 0;
}
.fleet-check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  height: var(--control-height);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.fleet-apply { height: var(--control-height); }
.fleet-apply[hidden] { display: none; }

.fleet { margin-top: var(--space-lg); }
/* What the list holds on the left, the one sort control on the right. */
.fleet-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-lg);
}
.fleet-head-what {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  min-width: 0;
}
.fleet-count { margin: 0; font-size: var(--font-size-sm); color: var(--color-text-muted); }
/* The sort and, under Most active, its window (IW-634): one group, so the
 * window reads as part of the order rather than another filter. */
.fleet-order {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
}
.fleet-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.fleet-sort select {
  width: auto;
  height: var(--control-height);
  box-sizing: border-box;
  padding-top: 0;
  padding-bottom: 0;
  font-weight: 600;
}
.fleet-count-n {
  color: var(--color-text-primary);
  font-size: var(--font-size-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.fleet-explain {
  margin: var(--space-xs) 0 var(--space-md);
  max-width: var(--measure-wide);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* Active filters as chips: each says what it holds and is its own clear. */
.fleet-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}
.fleet-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 2px var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  color: var(--color-text-primary);
  font-size: var(--font-size-xs);
  text-decoration: none;
}
.fleet-chip:hover { background: var(--color-bg-hover); }
/* The clear mark, drawn rather than typed, so no glyph varies by font. */
.fleet-chip-x {
  position: relative;
  width: 8px;
  height: 8px;
  flex: none;
}
.fleet-chip-x::before,
.fleet-chip-x::after {
  content: "";
  position: absolute;
  top: 3.5px;
  left: 0;
  width: 8px;
  height: 1px;
  background: var(--color-text-muted);
  transform: rotate(45deg);
}
.fleet-chip-x::after { transform: rotate(-45deg); }
.fleet-clear {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-decoration-color: var(--color-text-muted);
  text-underline-offset: 2px;
}
.fleet-clear:hover { color: var(--color-text-primary); }

.fleet-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--color-border-subtle); }
/* The strip's key sits on the rows' own grid, so it lines up over the strips. */
.fleet-key,
.fleet-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 200px 10.5rem;
  column-gap: var(--space-xl);
  padding-left: var(--space-md);
  padding-right: var(--space-md);
}
.fleet-key { padding-bottom: var(--space-xs); font-size: var(--font-size-xs); color: var(--color-text-muted); }
.fleet-key-strip { grid-column: 2; white-space: nowrap; }
.fleet-row {
  position: relative;
  align-items: center;
  row-gap: var(--space-xs);
  padding-top: var(--space-sm);
  padding-bottom: var(--space-sm);
  padding-left: calc(var(--space-md) - 3px);
  border-left: 3px solid transparent;
  border-bottom: 1px solid var(--color-border-subtle);
}
.fleet-row:hover { background: var(--color-bg-hover); }
.fleet-row-open { border-left-color: var(--color-status-warning); }
/* One focus ring for the whole row, from its one link. */
.fleet-row:has(.fleet-link:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}
.fleet-row .fleet-link:focus-visible { outline: none; }
.fleet-id { min-width: 0; }
.fleet-id p { margin: 0; }
.fleet-name-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-sm);
}
.fleet-link {
  color: var(--color-text-primary);
  font-size: var(--font-size-md);
  font-weight: 600;
  text-decoration: none;
  overflow-wrap: anywhere;
}
/* The name's link covers the row, so the whole row opens the boat page. */
.fleet-link::after { content: ""; position: absolute; inset: 0; }
.fleet-row:hover .fleet-link { text-decoration: underline; text-underline-offset: 2px; }
.fleet-name-line .reg-key { font-size: var(--font-size-xs); }
.fleet-open {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-status-warning);
  white-space: nowrap;
}
/* An open RMA card (IW-633): a part on its way, so the waiting tone rather
 * than the open ticket's warning, and its own words beside the ticket mark. */
.fleet-rma-open {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-status-waiting);
  white-space: nowrap;
}
.fleet-id .fleet-facts {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-md);
  margin-top: 2px;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.fleet-id .fleet-note { margin-top: 2px; font-size: var(--font-size-xs); color: var(--color-text-muted); }
.fleet-row-parked .fleet-link,
.fleet-row-parked .fleet-facts { color: var(--color-text-muted); }

/* 24 marks of 6px with 2px between and a 4px step at each of the two new
 * years 24 months can hold: 200px in all, the strip column's width in the
 * grid above. A literal, not a token: nothing outside the board reads it. */
.fleet-strip {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 22px;
  border-bottom: 1px solid var(--color-ledger-rail);
}
.fleet-mark {
  position: relative;
  flex: none;
  width: 6px;
  height: 0;
  border-radius: 1px 1px 0 0;
}
/* A new year is a tick down through the baseline, so the strip reads as a
 * timeline: where the burst falls, not only that there was one. */
.fleet-mark-year { margin-left: 4px; }
.fleet-mark-year::before {
  content: "";
  position: absolute;
  left: -4px;
  bottom: -4px;
  width: 1px;
  height: 7px;
  background: var(--color-ledger-rail);
}
.fleet-mark-1 { height: 7px; background: var(--color-text-secondary); }
.fleet-mark-2 { height: 14px; background: var(--color-text-secondary); }
.fleet-mark-3 { height: 21px; background: var(--color-text-secondary); }
.fleet-mark-open { background: var(--color-status-warning); }

.fleet-figures {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
.fleet-lead { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-primary); }
.fleet-lead-quiet { font-weight: 400; color: var(--color-text-muted); }

/* The customer sentiment mark (IW-615): a trim gauge, read like a boat's trim
 * indicator. A 40px rail with a notch at each whole score and a taller one at
 * neutral, angry at the left and pleased at the right, and the strip's own 6px
 * mark as the marker, at the weighted average to the nearest half point. Colour
 * arrives only off neutral: the fault colour below it, the resolved colour above.
 * Above the row's stretched link, so hover and focus reach it and its evidence. */
.fleet-mood {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  border-radius: var(--radius-sm);
  cursor: help;
}
.fleet-mood:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.fleet-gauge {
  position: relative;
  flex: none;
  width: 40px;
  height: 10px;
  border-bottom: 1px solid var(--color-ledger-rail);
  background:
    linear-gradient(var(--color-ledger-rail), var(--color-ledger-rail)) 0 100% / 1px 3px no-repeat,
    linear-gradient(var(--color-ledger-rail), var(--color-ledger-rail)) 25% 100% / 1px 3px no-repeat,
    linear-gradient(var(--color-ledger-rail), var(--color-ledger-rail)) 50% 100% / 1px 6px no-repeat,
    linear-gradient(var(--color-ledger-rail), var(--color-ledger-rail)) 75% 100% / 1px 3px no-repeat,
    linear-gradient(var(--color-ledger-rail), var(--color-ledger-rail)) 100% 100% / 1px 3px no-repeat;
}
.fleet-gauge-mark {
  position: absolute;
  bottom: 0;
  width: 6px;
  height: 9px;
  margin-left: -3px;
  border-radius: 1px 1px 0 0;
  background: var(--color-text-muted);
}
.fleet-gauge-0 .fleet-gauge-mark { left: 3px; }
.fleet-gauge-1 .fleet-gauge-mark { left: 12.5%; }
.fleet-gauge-2 .fleet-gauge-mark { left: 25%; }
.fleet-gauge-3 .fleet-gauge-mark { left: 37.5%; }
.fleet-gauge-4 .fleet-gauge-mark { left: 50%; }
.fleet-gauge-5 .fleet-gauge-mark { left: 62.5%; }
.fleet-gauge-6 .fleet-gauge-mark { left: 75%; }
.fleet-gauge-7 .fleet-gauge-mark { left: 87.5%; }
.fleet-gauge-8 .fleet-gauge-mark { left: calc(100% - 3px); }
.fleet-mood-neg .fleet-gauge-mark { background: var(--color-status-fault); }
.fleet-mood-pos .fleet-gauge-mark { background: var(--color-status-resolved); }
.fleet-mood-text { color: var(--color-text-secondary); }
.fleet-mood-neg .fleet-mood-text { color: var(--color-status-fault); }
.fleet-mood-lead .fleet-mood-text { font-size: var(--font-size-sm); font-weight: 600; }
.fleet-mood-lead.fleet-mood-mid .fleet-mood-text,
.fleet-mood-lead.fleet-mood-pos .fleet-mood-text { color: var(--color-text-primary); }
/* The evidence: the customer's own line, then where it came from. Opens under
 * the mark, held to the figures column's right edge so it never leaves the pane. */
.fleet-mood-tip {
  position: absolute;
  top: calc(100% + var(--space-xs));
  right: 0;
  z-index: 3;
  display: none;
  flex-direction: column;
  gap: var(--space-xs);
  width: max-content;
  max-width: min(20rem, 80vw);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg-surface);
  box-shadow: var(--shadow-raised);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: 400;
  text-align: left;
  white-space: normal;
}
.fleet-mood:hover .fleet-mood-tip,
.fleet-mood:focus-visible .fleet-mood-tip,
.fleet-mood:focus .fleet-mood-tip { display: flex; }
/* An open mark rises over the marks of the rows below, which its evidence covers. */
.fleet-mood:hover,
.fleet-mood:focus { z-index: 4; }
.fleet-mood-quote { color: var(--color-text-primary); font-size: var(--font-size-sm); font-style: italic; }
.fleet-empty a { color: var(--color-text-primary); }

/* A narrow pane drops the strip before it squeezes the names. */
@container pane (max-width: 720px) {
  .fleet-row { grid-template-columns: minmax(0, 1fr) auto; }
  .fleet-strip, .fleet-key { display: none; }
}
/* Phone width: the name, the open mark, the ranking figure and the last
 * contact, on two lines, with nothing to scroll sideways. */
@container pane (max-width: 560px) {
  .fleet-row { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--space-md); }
  /* Sort over Window, labels in one column and controls in the next, so the
   * two selects start on one edge; the window keeps its own compact width. */
  .fleet-order {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    width: 100%;
  }
  /* Each label keeps its own box, so its select keeps its name; subgrid lines
   * its two parts up on the group's columns. */
  .fleet-order .fleet-sort {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
  }
  .fleet-sort select { min-width: 0; }
  .fleet-window select { justify-self: start; }
  .fleet-id .fleet-facts, .fleet-id .fleet-note, .fleet-name-line .reg-key, .fleet-fig { display: none; }
  .fleet-figures { white-space: normal; }
  .fleet-controls { gap: var(--space-sm); }
  .fleet-filters { width: 100%; }
  .fleet-control { flex: 1 1 calc(50% - var(--space-md)); min-width: 0; }
  .fleet-control select { width: 100%; max-width: none; }
}
@media (forced-colors: active) {
  .fleet-mark-1, .fleet-mark-2, .fleet-mark-3 { background: CanvasText; }
  .fleet-mark-open { background: Highlight; }
  .fleet-row-open { border-left-color: Highlight; }
  .fleet-gauge-mark, .fleet-mood-pos .fleet-gauge-mark { background: CanvasText; }
  .fleet-mood-neg .fleet-gauge-mark { background: Highlight; }
}
.reg-actions { display: flex; flex-wrap: wrap; gap: var(--space-md); margin-top: var(--space-md); }
.reg-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-sm);
  flex: 1 1 280px;
}
.reg-form label { display: flex; flex-direction: column; gap: 2px; color: var(--color-text-secondary); }
.reg-form label[hidden] { display: none; }
/* Record the installer (IW-518) borrows the New boat card's inline radios.
 * The form's own label rule above stacks every label, so the choice labels
 * are put back on one line here, at the specificity that rule holds. */
.reg-form .rq-choice { flex-direction: row; align-items: baseline; gap: var(--space-xs); color: var(--color-text-primary); }
.reg-form .rq-choice[hidden] { display: none; }
.reg-form .cr-pick-label { color: var(--color-text-secondary); }
/* Change who is on this boat (IW-735): the New boat card's people block
 * inside a boat page form. The form takes the section's width rather than a
 * third of it, since the block's two columns and its sentence need the room
 * the card gives them, and stops at the width a form of four short rows
 * reads well at. The block's own rule draws a hairline above it to part it
 * from a card's title; here the form's title does that, so the line goes.
 * The block's labels are <label> elements, which the form's own label rule
 * above stacks; they are a grid column here, so they are put back inline. */
.reg-people-form { flex-basis: 100%; max-width: 44rem; }
.reg-people-form .rq-people { margin-top: 0; padding-top: 0; border-top: 0; }
.reg-people-form .rq-people-title { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.reg-people-form .rq-people-label { display: block; }
.reg-people-form .reg-people-fields { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-md); margin-top: var(--space-sm); }
.reg-people-form .reg-people-fields label { flex: 1 1 12rem; }
.reg-people-form .btn-primary { align-self: flex-start; }
/* ── The boat page as a profile (IW-736), laid out as Particulars (IW-815) ───
 * Direction A of the IW-815 mockups. The page opens on a plate, the one boxed
 * thing on it: the boat's name, one line of what else is on file, and four
 * cells for what a read in ten seconds lands on (system, where, owner, who
 * looks after it), each holding its value or a dashed Fill in, under a heavy
 * rule in the secondary ink (the registry's family hues belong to the proposal
 * chips alone, IW-529). Still to fill in folds into the plate's last band
 * as one line with a meter, open on a boat missing most of its values. Under
 * the plate the record runs in two columns, what is aboard and the boat's log,
 * with headings in sentence case on a hairline. Every value is still edited
 * where it is shown and every control is still shut behind a quiet summary. */
.reg-said {
  margin: var(--space-sm) 0 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-prose);
  line-height: 1.5;
  color: var(--color-text-secondary);
}
.reg-plate {
  display: block;
  margin-top: var(--space-sm);
  padding: var(--space-xl) var(--space-xl) 0;
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-top: 4px solid var(--color-text-secondary);
  border-radius: var(--radius-md);
}
.reg-plate .reg-title {
  font-size: 2rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
}
.reg-plate .reg-key {
  margin-left: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--color-text-muted);
  vertical-align: 0.45em;
}
.reg-plate-cells {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: var(--space-xl) calc(-1 * var(--space-xl)) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.reg-plate-cell {
  min-width: 0;
  padding: var(--space-md) var(--space-xl);
  border-left: 1px solid var(--color-border-subtle);
}
.reg-plate-cell:first-child { border-left: 0; }
.reg-plate-cell dt { margin-bottom: var(--space-xs); font-size: var(--font-size-xs); color: var(--color-text-muted); }
.reg-plate-cell dd { margin: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.reg-plate-val {
  font-size: var(--font-size-prose);
  font-weight: 600;
  color: var(--color-text-primary);
  overflow-wrap: anywhere;
}
.reg-plate-sub { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.reg-plate-link { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
.reg-plate-link:hover { color: var(--color-accent); }
.reg-plate-fill {
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-accent);
  background: none;
  border: 1px dashed var(--color-accent);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-sm);
  cursor: pointer;
}
.reg-plate-fill:hover { background: var(--color-bg-selected); }
.reg-plate-fill:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* A waiting answer in a plate cell (IW-839): what the checklist line holds,
 * shown where it is first seen. The value sits at the plate's size in the
 * secondary ink at the regular weight, so it reads as offered rather than on
 * file, with its source under it, then the line's own Accept and the quieter
 * Something else on one row. Accepted, the value takes the plate's own
 * weight and ink and the row goes. */
.reg-plate-offer {
  font-size: var(--font-size-prose);
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.reg-plate-src { font-size: var(--font-size-xs); color: var(--color-text-muted); overflow-wrap: anywhere; }
.reg-plate-acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-xs);
}
.reg-plate-acts .btn-secondary { padding: 3px var(--space-md); }
.reg-plate-acts .btn-link { color: var(--color-accent); opacity: 1; font-size: var(--font-size-xs); }
.reg-plate-status { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
.reg-plate-status:empty { display: none; }
/* The checklist, the plate's last band: one line that opens it. */
.reg-check-strip {
  margin: 0 calc(-1 * var(--space-xl));
  border: 0;
  border-top: 1px solid var(--color-border-subtle);
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  background: var(--color-bg-page);
  font-size: var(--font-size-sm);
}
.reg-check-sum {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-xl);
  list-style: none;
  cursor: pointer;
}
.reg-check-strip > .reg-check-sum {
  font-size: var(--font-size-sm);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  background: none;
  border-radius: 0;
}
.reg-check-strip > .reg-check-sum::before { content: none; }
.reg-check-sum::marker { content: ""; }
.reg-check-sum:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.reg-check-meter {
  flex: 0 0 9rem;
  height: 4px;
  border-radius: 2px;
  background: var(--color-bg-hover);
  overflow: hidden;
}
.reg-check-meter-fill { display: block; height: 100%; background: var(--color-text-secondary); }
.reg-check-done { margin: 0; color: var(--color-text-secondary); white-space: nowrap; }
.reg-check-sum .reg-check-title { margin-left: auto; font-weight: 600; color: var(--color-accent); white-space: nowrap; }
.reg-check-title::after { content: "\25BE"; display: inline-block; margin-left: var(--space-xs); }
.reg-check-strip[open] .reg-check-title::after { transform: rotate(180deg); }
.reg-check-n { font-variant-numeric: tabular-nums; }
.reg-check-body { padding: 0 var(--space-xl) var(--space-lg); }
.reg-check-body .reg-check-list:not(.reg-check-chips) {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-xl);
}
/* A field with nothing held is one dashed button; its editor opens under the
 * buttons, across the band. */
.reg-check-chips { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.reg-check-chips .reg-check-item { display: block; padding: 0; border: 0; }
.reg-check-chips .reg-check-item:has(.reg-inline:not([hidden])) { flex-basis: 100%; }
.reg-check-chips .reg-form.reg-inline { margin-top: var(--space-sm); }
.reg-check-chip {
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
  background: var(--color-bg-surface);
  border: 1px dashed var(--color-text-muted);
  border-radius: var(--radius-sm);
  padding: 3px var(--space-sm);
  cursor: pointer;
}
.reg-check-chip:hover,
.reg-check-chip:focus-visible { border-style: solid; border-color: var(--color-accent); color: var(--color-accent); }
.reg-check-chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.reg-check-note { margin: var(--space-md) 0 0; font-size: var(--font-size-xs); color: var(--color-text-muted); }
/* The record under the plate: two columns where each gets a reading width,
 * else one, headings in sentence case on a hairline. */
.reg-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
  gap: 0 2.5rem;
  margin-top: var(--space-sm);
}
.reg-col { min-width: 0; }
.reg-boat-page .reg-section { margin: var(--space-xl) 0 0; }
.reg-boat-page .reg-section-title {
  margin: 0 0 var(--space-sm);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-prose);
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  color: var(--color-text-primary);
}
.reg-section-n { margin-left: var(--space-xs); font-weight: 400; color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.reg-boat-page .reg-device-class { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-secondary); }
/* People as rows: the person, the part they play, when last seen. */
.reg-people-rows { list-style: none; margin: 0; padding: 0; font-size: var(--font-size-md); }
.reg-person-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--space-md);
  align-items: baseline;
  padding: var(--space-xs) 0;
}
.reg-person-role { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.reg-person-seen { font-size: var(--font-size-xs); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
/* A record row: the value in the text colour, its source after it, and
 * change as a quiet word that takes the accent only under the pointer. */
.reg-profile { grid-template-columns: 9rem minmax(0, 1fr); gap: var(--space-sm) var(--space-md); font-size: var(--font-size-md); }
.reg-profile dt { color: var(--color-text-muted); }
.reg-profile dd { position: relative; }
.reg-src { color: var(--color-text-muted); font-size: var(--font-size-xs); }
/* The location's confirmed country (IW-753), the header's flag at the same size
   and with the same hairline, before the place. */
.reg-flag {
  width: 16px;
  height: 12px;
  margin-right: var(--space-xs);
  vertical-align: -1px;
  border-radius: 1px;
  box-shadow: 0 0 0 1px var(--color-border-subtle);
}
.reg-edit {
  color: var(--color-text-muted);
  font: inherit;
  font-size: var(--font-size-xs);
  background: none;
  border: 0;
  padding: 0 0 0 var(--space-xs);
  cursor: pointer;
}
.reg-value:hover .reg-edit,
.reg-edit:focus-visible { color: var(--color-accent); }
.reg-edit:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.reg-empty-section { color: var(--color-text-muted); font-size: var(--font-size-sm); margin: 0; }
/* The inline editor, under the value it corrects or its checklist line: the
 * page's form, at the width of a short value, with Save and Cancel on one
 * line. Hidden until change or Fill in opens it. */
.reg-form.reg-inline { margin: var(--space-xs) 0 var(--space-sm); max-width: 28rem; flex: none; }
/* The boat's No Foreign Land link (IW-754): the link in the value's place, which
 * may break anywhere on a phone rather than push the row wide, and remove as a
 * quiet word beside change, not a form box. */
.reg-nfl-link { color: var(--color-text-primary); overflow-wrap: anywhere; text-decoration: none; }
.reg-nfl-link:hover { color: var(--color-accent); text-decoration: underline; }
.reg-nfl-search { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
.reg-nfl-search:hover { color: var(--color-accent); }
.reg-form.reg-nfl-remove {
  display: inline;
  padding: 0;
  border: 0;
  background: none;
  font-size: inherit;
}
.reg-form.reg-nfl-remove .reg-status { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
.reg-form.reg-inline[hidden] { display: none; }
.reg-inline-actions { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.reg-inline-actions .btn-link { color: var(--color-text-muted); opacity: 1; padding: 0; }
.reg-inline-actions .btn-link:hover { color: var(--color-text-primary); }
/* A control that is not a value, shut behind one line in the muted tone:
 * the serial form, the partner form, the people form, Park. The summary
 * takes the page's disclosure register; the form inside keeps its own box. */
.reg-quiet { border: 0; margin: var(--space-sm) 0 0; }
.reg-quiet > summary {
  display: inline-block;
  padding: 0;
  background: none;
  cursor: pointer;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.reg-quiet > summary:hover { color: var(--color-text-primary); }
.reg-quiet[open] > summary { color: var(--color-text-primary); margin-bottom: var(--space-sm); }
/* The device and ticket row controls (Set side, Add its ID, Mark removed,
 * Not this boat) take the same quiet line on this page, so every shut
 * control reads alike; the form inside keeps its own box. */
.reg-boat-page .reg-side > summary {
  display: inline-block;
  padding: 0;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.reg-boat-page .reg-side > summary:hover,
.reg-boat-page .reg-side[open] > summary { color: var(--color-text-primary); }
.reg-boat-page .reg-side { background: none; border: 0; padding: 0; }
/* A value's history ("1 earlier value") takes the same quiet line. */
.reg-boat-page .reg-history > summary {
  display: inline-block;
  padding: 0;
  background: none;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.reg-boat-page .reg-history > summary:hover { color: var(--color-text-primary); }
.reg-boat-page .reg-history { background: none; border: 0; padding: 0; }
.reg-quiet > .reg-form { max-width: 28rem; }
.reg-quiet > .reg-people-form { max-width: 44rem; }
.reg-quiet .reg-form + .reg-meta { margin-top: var(--space-xs); }
.reg-park { margin-top: var(--space-xl); }
/* Support history as a log (IW-815): the day it was linked in a gutter, the
 * ticket in figures, how it came to be here, and Not this boat under that. */
.reg-tickets { list-style: none; margin: 0; padding: 0; font-size: var(--font-size-md); }
.reg-ticket {
  display: grid;
  grid-template-columns: 6.5rem 4rem minmax(0, 1fr);
  gap: var(--space-xs) var(--space-md);
  align-items: baseline;
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.reg-ticket:first-child { border-top: 0; }
.reg-ticket-at { font-size: var(--font-size-sm); color: var(--color-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.reg-ticket-id { font-family: var(--font-mono); font-size: var(--font-size-sm); }
.reg-ticket-id a { color: var(--color-text-primary); text-decoration: none; }
.reg-ticket-id a:hover { color: var(--color-accent); text-decoration: underline; }
.reg-how { color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.reg-not-this-boat { grid-column: 3; }
.reg-not-this-boat[open] { grid-column: 1 / -1; }
/* Tickets marked Not this boat from Show boat history (IW-776): the same rows, the
 * day it was marked in the gutter, with Undo beside the line it undoes. */
.reg-device-class.reg-marked-title { margin-top: var(--space-md); }
.reg-marked .reg-ticket { grid-template-columns: 6.5rem 4rem minmax(0, 1fr) auto; }
.reg-marked .reg-side-form { flex: 0 0 auto; padding: 0; }
.reg-rmas { margin: var(--space-sm) 0 0; }
/* Filled in from ticket text (IW-829): the Support history log line, with
 * the field and its value where the history says how a ticket came, and a
 * quiet warning line under it when the value never reached Freshdesk. */
.reg-fills-lede { margin: 0 0 var(--space-xs); max-width: 60ch; font-size: var(--font-size-xs); color: var(--color-text-muted); }
.reg-fill-what { min-width: 0; overflow-wrap: anywhere; }
.reg-fill-field { color: var(--color-text-secondary); font-size: var(--font-size-sm); margin-right: var(--space-xs); }
.reg-fill-value { color: var(--color-text-primary); font-weight: 600; }
.reg-fill-unsynced { display: block; margin-top: 2px; font-size: var(--font-size-xs); color: var(--color-status-warning); }
.reg-owner-rows { margin-top: var(--space-sm); }
.reg-check-why { margin: var(--space-xs) 0 var(--space-sm); color: var(--color-text-muted); font-size: var(--font-size-xs); }
.reg-check-list { list-style: none; margin: 0; padding: 0; }
.reg-check-item {
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--color-border-subtle);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--space-sm);
  align-items: center;
}
.reg-check-field { color: var(--color-text-secondary); grid-column: 1; }
.reg-check-value { color: var(--color-text-primary); grid-column: 1; }
.reg-check-src { color: var(--color-text-muted); font-size: var(--font-size-xs); grid-column: 1; }
/* The line's answers, on the right: Accept, then the quieter ways out
 * (another value, or dismissing the proposal behind the line), stacked. */
.reg-check-acts {
  grid-column: 2;
  grid-row: 1 / span 3;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-xs);
}
.reg-check-acts .btn-link { color: var(--color-accent); opacity: 1; padding: 0; font-size: var(--font-size-xs); }
.reg-check-acts .btn-link:hover { text-decoration: underline; }
.reg-check-acts .btn-link.reg-check-dismiss { color: var(--color-text-muted); }
.reg-check-acts .btn-link.reg-check-dismiss:hover { color: var(--color-text-primary); }
.reg-check-status { grid-column: 1 / -1; font-size: var(--font-size-xs); color: var(--color-text-secondary); }
.reg-check-status:empty { display: none; }
.reg-check-item .reg-form.reg-inline { grid-column: 1 / -1; max-width: none; }
.reg-check-item.reg-check-recorded .reg-check-value,
.reg-check-item.reg-check-recorded .reg-check-field { color: var(--color-text-muted); }
.reg-check-item.reg-check-recorded .reg-check-acts { display: none; }
@container pane (max-width: 720px) {
  .reg-plate { padding: var(--space-lg) var(--space-lg) 0; }
  .reg-plate .reg-title { font-size: 1.6rem; }
  .reg-plate-cells { grid-template-columns: repeat(2, minmax(0, 1fr)); margin: var(--space-lg) calc(-1 * var(--space-lg)) 0; }
  .reg-plate-cell { padding: var(--space-md) var(--space-lg); }
  .reg-plate-cell:nth-child(3) { border-left: 0; }
  .reg-plate-cell:nth-child(n + 3) { border-top: 1px solid var(--color-border-subtle); }
  .reg-check-strip { margin: 0 calc(-1 * var(--space-lg)); }
  .reg-check-sum { flex-wrap: wrap; padding: var(--space-md) var(--space-lg); }
  .reg-check-meter { flex-basis: 100%; }
  .reg-check-body { padding: 0 var(--space-lg) var(--space-lg); }
  .reg-check-body .reg-check-list:not(.reg-check-chips) { grid-template-columns: minmax(0, 1fr); }
  .reg-profile { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
  .reg-profile dt { margin-top: var(--space-xs); }
  .reg-ticket { grid-template-columns: 4rem minmax(0, 1fr); }
  .reg-ticket-at { grid-column: 1 / -1; }
  .reg-not-this-boat { grid-column: 1 / -1; }
  .reg-marked .reg-ticket { grid-template-columns: 4rem minmax(0, 1fr) auto; }
  .reg-person-row { grid-template-columns: minmax(0, 1fr) auto; }
  .reg-person-role { grid-column: 1; grid-row: 2; }
  .reg-person-seen { grid-column: 2; grid-row: 1 / span 2; }
}
@media (prefers-reduced-motion: no-preference) {
  .reg-check-item.reg-check-recorded { transition: color 200ms ease; }
}

/* What the ticket side holds that the registry has not settled: a note after
 * the value, muted like every other aside on the page. */
.reg-installer-note { display: block; margin-top: 2px; color: var(--color-text-muted); }
/* The slot's history under the line, the identity list's own register at the
 * width of that list's value column rather than the page's. */
.reg-installer-history { max-width: 28rem; margin: 0 0 var(--space-sm); }
.reg-form-title { font-size: var(--font-size-sm); margin: 0; }
.reg-status { color: var(--color-text-muted); font-size: var(--font-size-sm); }
/* The candidate card's boat picker (IW-321). It is the Boats tab's suggestion
 * box in pick mode, so .reg-suggest and its items are reused as they stand and
 * only the host is new. The search wrapper is the containing block for the
 * list, the job .reg-lookup-field does on the Boats tab.
 * The outcome sentence is the card's one emphasis: it is what the button will
 * do, so it reads at the card's own text size in the primary colour while the
 * label above it stays secondary. The chosen boat borrows .cr-name and
 * .cr-owner's treatment, so a pick reads as one record joining another.
 * Every rule that sets display also answers [hidden], which a class rule
 * would otherwise override. */
.cr-pick {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.cr-pick-label {
  display: block;
  margin-bottom: var(--space-xs);
  color: var(--color-text-secondary);
}
.cr-pick-label[hidden] { display: none; }
.cr-pick-search { position: relative; max-width: 28rem; }
.cr-pick-q { width: 100%; }
.cr-pick-chosen {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
}
.cr-pick-chosen[hidden] { display: none; }
.cr-pick-chosen-name { font-weight: 600; }
.cr-pick-chosen-meta { color: var(--color-text-secondary); }
/* .btn-link is colored for the inverse header; this sits on a card. */
.cr-pick-clear { color: var(--color-accent); opacity: 1; padding: 0; }
.cr-outcome { margin: var(--space-xs) 0 0; color: var(--color-text-primary); max-width: 38rem; }
.cr-outcome-boat { font-weight: 600; }
/* A boat with no name on file (IW-355), wherever registry_suggest.js shows a
 * name: the suggestion row, the chosen boat, the merge sentence. Out of the
 * name's bold weight, so the blank does not read as a boat called that. It
 * follows the three name rules because it has to outrank them at equal
 * specificity. Inside the sentence it keeps the sentence's own colour. */
.reg-name-absent { font-weight: 400; color: var(--color-text-muted); }
.cr-outcome-boat.reg-name-absent { color: inherit; }
/* Same control height as the Boats tab's lookup row (IW-291) so the two tabs match. */
.cr-actions .btn-primary,
.cr-actions .btn-secondary {
  height: var(--control-height);
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
}
/* The rule above sets display, which outranks the hidden attribute: without
 * this, Cancel showed at rest beside Erase, outside the confirming state it
 * belongs to (found while building IW-321). */
.cr-actions .btn-primary[hidden],
.cr-actions .btn-secondary[hidden] { display: none; }

/* ── A disabled link button (IW-413) ──────────────────────────────────────
 * The link button is inverse-coloured at its base and recoloured for each
 * surface it lands on: cards by .rq-address-toggle, .cr-danger and
 * .cr-pick-clear, the admin console by a scoped rule whose hover and danger
 * variants weigh three classes. IW-358 and IW-367 gave the other buttons a
 * disabled look; this one kept its colour and its hover lift, so a dead
 * "Choose a different boat" read as live while a candidate was merging.
 * The disabled state has to beat every recolour at rest and under the pointer,
 * so this rule sits after the last of them and names the attribute as well as
 * the state so it weighs three classes: it ties the heaviest recolour and wins
 * on order. Same grammar as the outline and secondary buttons, label to muted
 * and nothing left for hover to lift. The underline stays, because it is the
 * control's shape, as a border is the outline button's. A recolour heavier
 * than three classes, or one at that weight added below this rule, undoes it:
 * the suite reads the colour the browser settles on for each surface. */
.btn-link[disabled]:disabled {
  color: var(--color-text-muted);
  opacity: 1;
  cursor: default;
}

/* The boat page's Devices section (IW-567). One quiet head per class, then a line per
   device: the class in words and the ID in the mono weight IDs take everywhere in the
   registry, with its serial and where it was seen in the muted meta tone. A former
   device keeps its line, dimmed, with the word said, so the history reads without a
   second list. The serials nobody has attributed follow, each a small form like the
   others on this page. */
.reg-device-group + .reg-device-group { margin-top: var(--space-md); }
.reg-device-class {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.reg-device-list { list-style: none; margin: 0; padding: 0; font-size: var(--font-size-sm); }
/* The tickets a name match put on the boat (IW-734), under Support history: the
   device list's own rows, set off from the list of tickets above them. */
.reg-name-links { margin-top: var(--space-sm); }
.reg-device {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px var(--space-sm);
  padding: var(--space-xs) 0;
}
.reg-device + .reg-device { border-top: 1px dashed var(--color-border-subtle); }
.reg-device-name { color: var(--color-text-primary); }
.reg-device-id { font-family: var(--font-mono); font-weight: 600; }
.reg-device-former .reg-device-name { color: var(--color-text-secondary); }
.reg-device-state {
  padding: 0 var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.reg-device-serial { color: var(--color-text-secondary); }
.reg-device-seen { flex-basis: 100%; }
/* A controller's side (IW-650). Said in plain words beside the controller, in the
   weight a fact on this line takes. The control that sets it stays shut under the
   line, behind the same quiet summary an earlier serial value opens from, so a list
   of controllers reads as a list and not as a column of selects. Opened, it is the
   page's form without its box, the label beside its select rather than over it. */
.reg-device-side { color: var(--color-text-primary); font-weight: 600; }
/* As wide as its own words, not the row: three controllers are not three bars. */
.reg-side { flex: 0 1 auto; max-width: 100%; }
.reg-device-former .reg-device-side { color: var(--color-text-secondary); }
.reg-side-form {
  flex: 1 1 100%;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  /* Inset to the summary's own edge, so the control lines up under its words. */
  padding: 0 var(--space-md) var(--space-sm);
  border: 0;
  background: none;
}
.reg-form.reg-side-form label { flex-direction: row; align-items: center; gap: var(--space-xs); }
/* The ID of a device recorded with none (IW-682), in the Side control's form. One line
   says what the ID is, at a reading measure and inset to the summary's edge like the
   form under it; the box is as wide as the four characters it takes, in the mono face
   an ID has everywhere here, and its label stays on one line beside it. */
.reg-device-id-note { margin: 0 var(--space-md) var(--space-xs); max-width: 60ch; }
.reg-form.reg-side-form .reg-device-id-input {
  flex: none;
  width: 8ch;
  font-family: var(--font-mono);
  text-transform: uppercase;
}
.reg-device-id-add label { white-space: nowrap; }
.reg-attribute-new[hidden] { display: none; }
/* The device prompt's card on Learnings and the boat page (IW-567): what it asks,
   and a link to the ticket where its photo and choices are. */
.rq-device-lines { margin: var(--space-xs) 0 0; padding-left: var(--space-md); color: var(--color-text-secondary); }
.rq-device-answer { margin: var(--space-xs) 0 0; }
/* Teams channels linked to boats (IW-375). The history review's ledger, with one
   device of its own: the match block under a channel's name. The Boat box holds
   the proposed boat, and under it what matched it, in the confidence colours: a
   hull code (exact) reads high, a boat name middle, and an owner alone low, so the
   eye can pass over the hull matches and slow down on the owner ones. (The boat
   family colour stays the proposal chips' own.) A left rule
   in the same colour ties the box to its evidence; a channel nothing matched has
   a quiet rule and says so. */
.tc-source {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  margin: var(--space-md) 0;
  font-size: var(--font-size-sm);
}
.tc-source-line { color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.tc-source-line:empty { display: none; }
.tc-source .btn-secondary { min-height: var(--control-height); }
.tc-source-status { color: var(--color-text-secondary); }
.tc-source-status:empty { display: none; }
.tc-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-xs) var(--space-md); }
.tc-head .hr-name { flex: 1 1 16rem; }
.tc-open { font-size: var(--font-size-sm); color: var(--color-text-secondary); white-space: nowrap; }
.tc-description { margin: 2px 0 0; color: var(--color-text-muted); font-size: var(--font-size-sm); overflow-wrap: anywhere; }
.tc-match {
  margin: var(--space-sm) 0 0;
  padding-left: var(--space-md);
  border-left: 2px solid var(--color-confidence-middle);
}
.tc-match-none { border-left-color: var(--color-border-subtle); }
.tc-pick { display: flex; align-items: center; gap: var(--space-sm); }
.tc-pick-label { color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.tc-pick-input { flex: 1 1 auto; max-width: 26rem; height: var(--control-height); padding: 0 var(--space-sm); }
.tc-why { margin: var(--space-xs) 0 0; color: var(--color-text-muted); font-size: var(--font-size-sm); }
.tc-basis { color: var(--color-confidence-middle); font-weight: 600; }
.tc-match-hull { border-left-color: var(--color-confidence-high); }
.tc-match-hull .tc-basis { color: var(--color-confidence-high); }
.tc-match-owner { border-left-color: var(--color-confidence-low); }
.tc-match-owner .tc-basis { color: var(--color-confidence-low); }
.tc-boat { color: var(--color-text-primary); font-weight: 600; overflow-wrap: anywhere; }
/* The settled lists fold under a plain heading, not the brief's boxed section
   register: they are the record of decisions already made, read rarely. */
.tc-group {
  margin: var(--space-xl) 0 0;
  border: none;
  border-top: 1px solid var(--color-border-subtle);
  border-radius: 0;
  padding-top: var(--space-md);
}
.tc-group > summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: 0;
  background: none;
  border-radius: 0;
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.tc-group > summary::before,
.tc-group[open] > summary::before { content: none; }
.tc-group > summary::after {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid var(--color-text-muted);
  border-bottom: 1.5px solid var(--color-text-muted);
  transform: translateY(-3px) rotate(45deg);
}
.tc-group[open] > summary::after { transform: translateY(0) rotate(-135deg); }
.tc-group > summary:hover,
.tc-group[open] > summary { color: var(--color-text-primary); }
.tc-group > summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.tc-settled-name a { color: var(--color-text-primary); }
.tc-group-lede { margin: var(--space-sm) 0 0; color: var(--color-text-muted); font-size: var(--font-size-sm); max-width: 62ch; }
.tc-settled-list { list-style: none; margin: var(--space-sm) 0 0; padding: 0; }
.tc-settled {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-md);
}
.tc-settled-name { flex: 1 1 16rem; color: var(--color-text-secondary); overflow-wrap: anywhere; }
.tc-settled .btn-link { padding: 0; font-size: var(--font-size-sm); color: var(--color-accent); opacity: 1; text-decoration: underline; }
.tc-settled .btn-link[hidden] { display: none; }
.tc-settled-status { flex: 1 1 100%; color: var(--color-text-muted); font-size: var(--font-size-sm); }
.tc-settled-status:empty { display: none; }
/* A link Integra made by itself (IW-957): a quiet line under the row saying what
   matched, its first words in the match block's confidence colour (a hull code
   high, a boat name middle), so the eye can find the links nobody checked. */
.tc-group-note { color: var(--color-text-muted); font-size: var(--font-size-sm); font-weight: 400; }
.tc-auto { flex: 1 1 100%; color: var(--color-text-muted); font-size: var(--font-size-sm); overflow-wrap: anywhere; }
.tc-auto-who { color: var(--color-confidence-middle); font-weight: 600; }
.tc-auto-hull .tc-auto-who { color: var(--color-confidence-high); }
@container pane (max-width: 560px) {
  .tc-pick-input { max-width: none; }
}

/* ════════════════════════════════════════════════════════════════════
 * Product guides inside the workbench (IW-625)
 * ════════════════════════════════════════════════════════════════════
 * A guide's content is the file in docs/ as written, so these rules style its
 * own vocabulary: the masthead, numbered sections, panels, flow steps, chips,
 * tags, tables and diagrams. They give it the workbench's look in place of the
 * file's stylesheet, which stays behind: IBM Plex, the reading scale the chat
 * uses for prose, the semantic colours, and the chat's table register. The
 * rail lists the sections, so the guide's own contents panel is not drawn.
 *
 * Guide markup names the file's palette inline in a few places (a glyph drawn
 * in the accent, say). Those names are given workbench values here, scoped to
 * the guide, so the inline use follows the theme like everything else. */
.guide-doc {
  --accent: var(--color-accent);
  --brand: var(--color-accent);
  --ink: var(--color-text-primary);
  --muted: var(--color-text-muted);
  --line: var(--color-border-subtle);
  --card: var(--color-bg-surface);
  --wash: var(--color-bg-hover);
  --ok: var(--color-status-resolved);
  --gate: var(--color-status-warning);
  max-width: var(--measure-wide);
  margin: 0 auto;
  padding: var(--space-md) 0 48px;
  font-size: var(--font-size-prose);
  line-height: var(--line-height-prose);
  color: var(--color-text-primary);
}
.guide-doc p,
.guide-doc ul,
.guide-doc ol,
.guide-doc h3,
.guide-doc h4 { max-width: var(--measure-prose); }
.guide-doc p,
.release-notes p { margin: 0 0 var(--space-md); }
.guide-doc ul,
.guide-doc ol,
.release-notes ul { margin: 0 0 var(--space-md); padding-left: 1.4em; }
.guide-doc li,
.release-notes li { margin: var(--space-xs) 0; }
.guide-doc li::marker,
.release-notes li::marker { color: var(--color-text-muted); }
.guide-doc strong,
.release-notes strong { font-weight: 600; }
.guide-doc a { color: var(--color-accent); text-decoration: underline; }
.guide-doc a:hover { color: var(--color-accent-hover); }
.guide-doc a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* The masthead: the guide's name is the page's title, the standfirst its one
   paragraph of purpose, the meta line its date and scope in the muted register. */
.guide-doc .masthead,
.release-notes-head {
  padding-bottom: var(--space-xl);
  border-bottom: 1px solid var(--color-border-subtle);
}
.guide-doc .eyebrow {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.guide-doc h1,
.release-notes h1 {
  margin: 0 0 var(--space-md);
  font-size: 1.75rem;
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.guide-doc .standfirst,
.release-notes-lead {
  margin: 0;
  font-size: var(--font-size-claim);
  line-height: 1.55;
  color: var(--color-text-secondary);
}
.guide-doc .masthead .meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
}
.guide-doc .masthead .meta,
.release-notes-meta {
  margin-top: var(--space-lg);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* Sections: the unit the rail lists. The number is the guide's reading order,
   set in the mono face the workbench uses for identifiers and kept quiet, so the
   heading's words carry it. scroll-margin keeps a jump from the rail off the
   pane's top edge. */
.guide-doc section,
.release-part {
  padding: var(--space-xl) 0 var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  scroll-margin-top: var(--space-md);
}
.guide-doc section:last-of-type,
.release-part:last-child { border-bottom: 0; }
.guide-doc section h2,
.release-part h2 {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  max-width: var(--measure-prose);
  margin: 0 0 var(--space-md);
  font-size: 1.3rem;
  font-weight: 650;
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.guide-doc .sec-n {
  flex: none;
  min-width: 2ch;
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  font-weight: 500;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.guide-doc h3,
.release-chapter h3 {
  margin: var(--space-xl) 0 var(--space-sm);
  font-size: 1.05rem;
  font-weight: 600;
  scroll-margin-top: var(--space-md);
}
.guide-doc h4 { margin: 0 0 var(--space-xs); font-size: 1em; font-weight: 600; }

/* Code, keys and samples: the chat's code register. */
.guide-doc code,
.guide-doc kbd,
.release-notes code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  background: var(--color-bg-code);
  border-radius: var(--radius-sm);
  padding: 1px 4px;
}
.guide-doc kbd {
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-bottom-width: 2px;
}
.guide-doc pre {
  margin: 0 0 var(--space-md);
  padding: var(--space-md);
  background: var(--color-bg-code);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  font-size: var(--font-size-sm);
  line-height: 1.5;
}
.guide-doc pre code { background: none; padding: 0; font-size: inherit; }

/* Tables: the chat's instrument register, horizontal rules only and mono column
   heads. The wrapper scrolls a wide table inside the pane rather than the page. */
.guide-doc .tablewrap { overflow-x: auto; margin: 0 0 var(--space-md); }
.guide-doc table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-md);
  line-height: 1.45;
}
.guide-doc th,
.guide-doc td {
  border-bottom: 1px solid var(--color-border-subtle);
  padding: var(--space-sm) var(--space-md) var(--space-sm) 0;
  text-align: left;
  vertical-align: top;
}
.guide-doc th {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.guide-doc td code { white-space: nowrap; }
.guide-doc td.wrapcode code { white-space: normal; }

/* Panels: a quiet card with a status stripe that says what kind of aside it is.
   A note is information (the waiting tone), a gate is something that asks before
   it acts (the warning tone), ok is a safe default (the resolved tone). */
.guide-doc .panel {
  max-width: var(--measure-prose);
  margin: var(--space-lg) 0;
  padding: var(--space-md) var(--space-lg);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-status-waiting);
  border-radius: var(--radius-md);
}
.guide-doc .panel.gate { border-left-color: var(--color-status-warning); }
.guide-doc .panel.ok { border-left-color: var(--color-status-resolved); }
.guide-doc .panel .label {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.guide-doc .panel > :last-child { margin-bottom: 0; }

/* Flow steps: a real sequence, so numbered, on a hairline that runs from one
   step to the next. */
.guide-doc ol.flow {
  list-style: none;
  max-width: var(--measure-prose);
  margin: var(--space-lg) 0;
  padding: 0;
  counter-reset: step;
}
.guide-doc ol.flow > li {
  position: relative;
  counter-increment: step;
  margin: 0 0 0 13px;
  padding: 0 0 var(--space-lg) 32px;
  border-left: 1px solid var(--color-border-subtle);
}
.guide-doc ol.flow > li:last-child { border-left-color: transparent; padding-bottom: 0; }
.guide-doc ol.flow > li::before {
  content: counter(step);
  position: absolute;
  left: -13px;
  top: 0;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: var(--color-bg-page);
  border: 1px solid var(--color-border-subtle);
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.guide-doc ol.flow p { margin-bottom: var(--space-sm); }

/* Chips name a literal (a command, a state); tags mark whether an action asks
   first or runs on its own, in the same tones as the panels. The guide's chip is
   not the workbench's .chip badge (top of this file), so every property that
   badge sets is set again here. */
.guide-doc .chip {
  display: inline-block;
  padding: 1px var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: 400;
  color: var(--color-text-primary);
  background: var(--color-bg-hover);
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  white-space: nowrap;
}
.guide-doc .tag {
  display: inline-block;
  padding: 0 6px;
  border: 1px solid currentColor;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 600;
  vertical-align: 1px;
}
.guide-doc .tag.gated { color: var(--color-status-warning); }
.guide-doc .tag.auto { color: var(--color-status-resolved); }

/* Diagrams: the guide's SVGs drawn in the workbench's surfaces and type. The
   arrowheads carry a fill of their own in the file; the rule outranks it so they
   follow the theme with the wires. */
.guide-doc .diag { margin: var(--space-lg) 0 var(--space-xs); }
.guide-doc .diag svg { display: block; width: 100%; height: auto; }
.guide-doc .diag .box { fill: var(--color-bg-surface); stroke: var(--color-border-subtle); stroke-width: 1.5; }
.guide-doc .diag .hub { stroke: var(--color-accent); stroke-width: 2; }
.guide-doc .diag .group { fill: var(--color-bg-hover); stroke: var(--color-border-subtle); stroke-width: 1; }
.guide-doc .diag text { font-family: var(--font-sans); }
.guide-doc .diag .t { font-size: 14px; font-weight: 600; fill: var(--color-text-primary); }
.guide-doc .diag .s { font-size: 11.5px; fill: var(--color-text-muted); }
.guide-doc .diag .lbl { font-size: 11px; font-weight: 600; fill: var(--color-text-secondary); }
.guide-doc .diag .cap { font-size: 12.5px; fill: var(--color-text-muted); }
.guide-doc .diag .wire { stroke: var(--color-text-muted); stroke-width: 1.5; fill: none; }
.guide-doc .diag marker path { fill: var(--color-text-muted); }
.guide-doc .diag-caption { font-size: var(--font-size-sm); color: var(--color-text-muted); }

.guide-doc .colophon {
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

@container pane (max-width: 560px) {
  .guide-doc h1,
  .release-notes h1 { font-size: 1.45rem; }
  .guide-doc .standfirst,
  .release-notes-lead { font-size: var(--font-size-prose); }
  .guide-doc section h2,
  .release-part h2 { font-size: 1.15rem; }
}

/* ── What's new: the release notes, read like a short story (IW-741) ──
   RELEASE_NOTES.md shares the guides' reading register above (the heading steps,
   prose size, standfirst, meta line and section rule), held to the prose measure
   so a wide pane still reads as one column. Coming next and Now read as prose.
   Under Earlier each chapter's opening sentences stay prose, and its one line
   releases drop to the smaller, secondary meta register on a hairline, so a
   reader skimming the chapters can pass over the lists. No tokens of its own. */
.release-notes {
  max-width: var(--measure-prose);
  margin: 0 auto;
  padding: var(--space-md) 0 48px;
  font-size: var(--font-size-prose);
  line-height: var(--line-height-prose);
  color: var(--color-text-primary);
}
/* The head's two lines outrank the prose paragraph rule, as the guide's
   standfirst does, so the head keeps its own spacing. */
.release-notes-head .release-notes-lead { margin: 0; }
.release-notes-head .release-notes-meta { margin: var(--space-lg) 0 0; font-variant-numeric: tabular-nums; }
.release-notes strong { font-variant-numeric: tabular-nums; }
.release-chapter { scroll-margin-top: var(--space-md); }
.release-chapter ul {
  margin: var(--space-sm) 0 0;
  padding-left: var(--space-md);
  list-style: none;
  border-left: 2px solid var(--color-border-subtle);
  font-size: var(--font-size-sm);
  line-height: 1.55;
  color: var(--color-text-secondary);
}
.release-chapter li strong { color: var(--color-text-primary); }
.release-notes-empty .empty-title { margin: 0; }

/* ── Expected values: the Add or Edit control, the form, the page (IW-660) ──
   An operator looking at a diagnostic reading records the value it should
   show. Three surfaces, one register: instrument output, set in the judged
   table's own grid and sizes, with nothing here louder than a flagged cell.

   The control in an Expected cell is a word, not a button: muted at rest so a
   table of forty rows does not read as forty calls to action, the accent on
   hover and focus. It is kept out of a selection like the cell's spoken words,
   so a copied table is still the readings alone.

   The form is a dialog over the dimmed workbench. Its head is the reading's
   nameplate: the code in the mono figures the table sets it in, the name, then
   one row of what the form already knows (product, firmware, device, the
   reading on screen, the state read from it) as small labelled facts, because
   those five are what make a value belong where it is saved. Under it, what is
   on file, then the editor. The suggestion is the one emphasized thing in the
   editor: the selected tint with the accent keyline, the sentence saying where
   it came from, and the caveat in the muted register directly beneath, so the
   source is read before the number is trusted. A value someone else set while
   the form was open takes the fault tint, since it stopped a save.

   The dialog sits in the top layer, outside the case pane, so the pane's
   container steps cannot reach it and it asks the window nothing either: its
   width, its padding and its number fields are fluid, and on a phone it simply
   fills the screen less a small margin. */
.expectation-open {
  margin-left: var(--space-xs);
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-decoration: underline;
  text-decoration-color: var(--color-border-subtle);
  text-underline-offset: 2px;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}
.evidence-expected > .expectation-open:first-child { margin-left: 0; }
.expectation-open:hover,
.expectation-open:focus-visible {
  color: var(--color-accent);
  text-decoration-color: currentColor;
}
.expectation-saved {
  color: var(--color-status-resolved);
  font-size: var(--font-size-xs);
}

.xf {
  width: min(36rem, calc(100vw - 2 * var(--space-sm)));
  max-width: none;
  max-height: min(46rem, calc(100dvh - 2 * var(--space-sm)));
  padding: 0;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  color: var(--color-text-primary);
  box-shadow: var(--shadow-raised);
  overflow: hidden;
}
.xf::backdrop { background: var(--color-scrim); opacity: 0.7; }
.xf[open] { display: flex; }
@media (prefers-reduced-motion: no-preference) {
  .xf[open] { animation: photo-viewer-in var(--motion-enter) var(--ease-out); }
}
.xf-form {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
}
.xf-head {
  position: relative;
  padding: var(--space-lg) clamp(var(--space-lg), 4vw, var(--space-xl)) var(--space-md);
  border-bottom: 1px solid var(--color-border-subtle);
}
.xf-title {
  margin: 0;
  padding-right: var(--space-xl);
  font-size: var(--font-size-title);
  font-weight: 600;
  line-height: 1.3;
}
.xf-code {
  font-family: var(--font-mono);
  font-weight: 500;
  color: var(--color-text-muted);
}
.xf-close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: var(--control-height);
  height: var(--control-height);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-muted);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.xf-close:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
.xf-meaning {
  margin: 2px 0 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.xf-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-xl);
  margin: var(--space-md) 0 0;
}
.xf-fact { min-width: 0; }
.xf-fact dt {
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.xf-fact dd {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.xf-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-md) clamp(var(--space-lg), 4vw, var(--space-xl)) var(--space-lg);
}
.xf-body > * + * { margin-top: var(--space-lg); }
.xf-body > [hidden] { display: none; }
.xf-label {
  margin: 0 0 var(--space-xs);
  padding: 0;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.xf-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--color-border-subtle);
}
.xf-entry {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-xs) var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border-subtle);
}
.xf-entry.known-entry { padding-left: var(--space-sm); }
/* The entry open in the form below takes the accent rule even when it is a known
   condition (IW-765): the mark and the warning text still say what it is. */
.xf-entry-editing { box-shadow: inset 2px 0 0 var(--color-accent); padding-left: var(--space-sm); }
.xf-entry-text { flex: 1 1 14rem; min-width: 0; font-size: var(--font-size-sm); }
.xf-entry-value { font-weight: 600; font-variant-numeric: tabular-nums; }
.xf-entry-meta,
.xf-entry-note {
  display: block;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.xf-entry-note { color: var(--color-text-secondary); }
.xf-entry-actions { display: flex; flex: none; gap: var(--space-xs); }
.xf-retire {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}
.xf-retire input[type=text] { flex: 1 1 12rem; width: auto; min-width: 0; }
.xf-retire .btn-danger { padding: 6px var(--space-md); }
.xf-retire .xf-hint { flex: 1 1 100%; }
.xf-another { margin-top: var(--space-sm); }
.xf-editor {
  min-width: 0;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  border: none;
}
.xf-editor > * + * { margin-top: var(--space-md); }
.xf-editor > [hidden] { display: none; }
.xf-suggestion {
  padding: var(--space-sm) var(--space-md);
  border-left: 3px solid var(--color-accent);
  background: var(--color-bg-selected);
  font-size: var(--font-size-sm);
}
.xf-suggestion-head { margin: 0 0 var(--space-xs); font-weight: 600; }
.xf-suggestion-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
}
.xf-suggestion-row + .xf-suggestion-row {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border-subtle);
}
.xf-suggestion-text { flex: 1; min-width: 0; }
.xf-suggestion-line { display: block; font-variant-numeric: tabular-nums; }
.xf-suggestion-caveat {
  display: block;
  margin-top: 2px;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.xf-suggestion .btn-small { flex: none; background: var(--color-bg-raised); }
.xf-row { display: block; }
.xf-field-label {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
.xf-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
}
.xf-choice {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--font-size-sm);
  cursor: pointer;
}
.xf-choice input { margin: 0; accent-color: var(--color-accent); }
.xf-values {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-sm) var(--space-md);
}
.xf-num[hidden] { display: none; }
.xf-num input[type=number] {
  width: min(8.5rem, 36vw);
  font-variant-numeric: tabular-nums;
}
.xf-unit {
  padding-bottom: 7px;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.xf-source { display: block; margin-top: var(--space-sm); }
.xf-source[hidden], .xf-other[hidden], .xf-reason[hidden], .xf-kind[hidden] { display: none; }
.xf-hint {
  display: block;
  margin: var(--space-xs) 0 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  max-width: 62ch;
}
.xf-hint-error { color: var(--color-status-fault); }
.xf-conflict {
  padding: var(--space-sm) var(--space-md);
  border-left: 3px solid var(--color-status-fault);
  background: var(--color-status-fault-bg);
  font-size: var(--font-size-sm);
}
.xf-conflict p { margin: 0; }
.xf-conflict p + p { margin-top: var(--space-xs); color: var(--color-text-secondary); }
/* The earlier values fold away under the editor. The shared disclosure is a
   boxed panel with a small capital heading, which is right for a section of a
   brief and too heavy inside a form, so this one gives the box up and keeps the
   form's own label register. Each selector outranks the element rule it
   answers. */
.xf .xf-history {
  margin-bottom: 0;
  border: none;
  border-radius: 0;
  background: none;
  overflow: visible;
}
.xf .xf-history > summary {
  padding: 0;
  background: none;
  font-size: var(--font-size-sm);
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-text-secondary);
  cursor: pointer;
}
.xf .xf-history .xf-list { margin-top: var(--space-sm); }
.xf-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md) clamp(var(--space-lg), 4vw, var(--space-xl));
  border-top: 1px solid var(--color-border-subtle);
  background: var(--color-bg-surface);
}
.xf-error {
  flex: 1 1 100%;
  margin: 0;
  color: var(--color-status-fault);
  font-size: var(--font-size-sm);
}
.xf-error[hidden] { display: none; }
/* Condition chips (IW-661). "When it applies" is a set of chips chosen from the
   shared condition list, the operating states first. A chip is a checkbox in a
   label: the box itself is visually hidden but keeps focus and the keyboard, and
   the label carries its state, the subtle outline at rest and the accent outline
   on the selected tint when chosen, which is how the open firmware switch is
   marked. Under the chips, the rule of each one chosen, in the hint register. */
.xf-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}
.xf-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 3px var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  cursor: pointer;
}
.xf-chip input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.xf-chip:hover { color: var(--color-text-primary); border-color: var(--color-text-muted); }
.xf-chip:has(input:checked) {
  border-color: var(--color-accent);
  background: var(--color-bg-selected);
  color: var(--color-text-primary);
  font-weight: 600;
}
.xf-chip:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.xf-applies-rules {
  margin: var(--space-xs) 0 0;
  padding: 0;
  list-style: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  max-width: 62ch;
}
.xf-applies-rules[hidden] { display: none; }
.xf-applies-rule-name { font-weight: 600; color: var(--color-text-secondary); }
/* The admin's condition form (IW-661): one line per reading compared, device type,
   reading, comparison and value side by side, wrapping on a phone. */
.cf-tests { display: grid; gap: var(--space-sm); }
.cf-test {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
}
.cf-test select, .cf-test input { width: auto; min-width: 0; max-width: 100%; }
.cf-test [name="code"] { flex: 1 1 8rem; }
.cf-value input[type=number] { width: min(6.5rem, 36vw); font-variant-numeric: tabular-nums; }
.cf-value select { max-width: 16rem; }
.cf-test .cf-remove[hidden], .cf-join[hidden], .cf-retire[hidden], .cf-preset-row[hidden] { display: none; }
.cf-add { margin-top: var(--space-sm); }
/* The Expectations page. The heading carries how many readings have a value and
   how many suggestions wait, in tabular figures, the way a queue's heading
   carries its count. Product and firmware are two labelled switches in the
   sheet tab register (the accent rule under the open one), since they choose
   which sheet of readings is shown. The reading sheet itself (IW-834) is its own
   block at the end of this file. */
.expectations-page { max-width: 68rem; padding: var(--space-lg) 0; }
.expectations-page .learnings-lede { max-width: 62ch; font-size: var(--font-size-md); }
.xp-progress {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
}
.xp-scope {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-xl);
  margin-bottom: var(--space-lg);
  box-shadow: inset 0 -1px 0 var(--color-border-subtle);
}
.xp-scope-item { display: inline-flex; align-items: baseline; gap: var(--space-xs); }
.xp-scope-label {
  margin-right: var(--space-xs);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.xp-switch {
  padding: var(--space-xs) var(--space-sm);
  border-bottom: 2px solid transparent;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.xp-switch:hover { color: var(--color-text-primary); }
.xp-switch[aria-current="true"] {
  color: var(--color-text-primary);
  border-bottom-color: var(--color-accent);
}
.xp-table td { padding-top: var(--space-xs); padding-bottom: var(--space-xs); }
.xp-table td:first-child { font-family: var(--font-mono); white-space: nowrap; }
.xp-table td:nth-child(2) { font-weight: 600; }
.xp-entry + .xp-entry { margin-top: var(--space-xs); }
.xp-entry .evidence-expected-text { font-variant-numeric: tabular-nums; }
.xp-entry-meta {
  display: block;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.xp-action { width: 1%; white-space: nowrap; }
/* The Conditions sheet (IW-661): a table in the diagnostic table's register, with
   the rule in the Meaning column's muted type and the count of values that need it
   in figures. */
.xp-conditions td:first-child { font-family: inherit; font-weight: 600; white-space: normal; }
.xp-conditions td:nth-child(2) { font-weight: 400; }
.xp-conditions-used { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--color-text-secondary); }
.xp-conditions-add { margin-top: var(--space-sm); }
.xp-conditions-who { margin-top: var(--space-sm); }


/* ── Expectations: the reading sheet (IW-834) ──────────────────────────────────
   Every reading of the sheet as a compact list on the left, one reading's whole
   page on the right. The list marks a reading with a suggestion in the accent
   and one with a value on file in the muted register; the open one takes the
   selected background and the accent rule a rail item wears. On the sheet the
   band is the one bold place: a labelled axis, the value on file as a solid
   muted bar, Integra's suggestion as an accent outlined box, and a tick under
   the axis for each resolved ticket's reading, its scale labels outside the
   axis so a tick near an end never meets a number. Confirm is the one filled
   control, Change the outline, Decline a quiet link. On a phone the list and
   the sheet are two screens. Shipped semantic tokens only. */
.xc-auto {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin: calc(-1 * var(--space-xs)) 0 var(--space-lg);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}
.xc-auto::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-status-resolved);
  transform: translateY(-1px);
}
.xc-auto a { color: var(--color-text-secondary); }
.xc-code { font-family: var(--font-mono); font-size: var(--font-size-sm); color: var(--color-text-muted); white-space: nowrap; }
.xc-when { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.xc-meta { display: block; color: var(--color-text-muted); font-size: var(--font-size-xs); }
.xc-none { margin: 0; color: var(--color-text-muted); font-size: var(--font-size-sm); }

.xc-split { display: grid; grid-template-columns: 17rem minmax(0, 1fr); gap: var(--space-xl); align-items: start; }

/* The list */
.xc-list { position: sticky; top: var(--space-lg); max-height: calc(100vh - 2 * var(--space-lg)); overflow-y: auto; }
.xc-group { margin: 0 0 var(--space-md); }
.xc-group-head { display: flex; align-items: baseline; justify-content: space-between; margin: 0 0 2px; padding: 0 var(--space-sm); font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-secondary); }
.xc-group-count { color: var(--color-text-muted); font-weight: 400; font-size: var(--font-size-xs); font-variant-numeric: tabular-nums; }
.xc-count-src { display: none; }
.xc-group ul { margin: 0; padding: 0; list-style: none; }
.xc-row a { display: grid; grid-template-columns: 2.5rem 1fr auto; align-items: baseline; gap: var(--space-xs); padding: 3px var(--space-sm); border-radius: var(--radius-sm); font-size: var(--font-size-sm); color: var(--color-text-secondary); text-decoration: none; }
.xc-row a:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
.xc-row a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.xc-row.is-open a { background: var(--color-bg-selected); color: var(--color-text-primary); box-shadow: inset 2px 0 0 var(--color-accent); font-weight: 600; }
.xc-row.is-blank a { color: var(--color-text-muted); }
.xc-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xc-mark { font-size: var(--font-size-xs); font-weight: 400; white-space: nowrap; color: var(--color-text-muted); }
.xc-mark-suggested { color: var(--color-accent); font-weight: 600; }
.xc-mark-waiting { color: var(--color-status-waiting); }
.xc-blank { border: 0; }
.xc-blank > summary { padding: 3px var(--space-sm); background: none; color: var(--color-text-muted); font-size: var(--font-size-xs); font-weight: 400; text-transform: none; letter-spacing: normal; cursor: pointer; }
.xc-blank > summary:hover { color: var(--color-text-primary); }
.xc-foot { margin: var(--space-md) 0 0; padding: 0 var(--space-sm); color: var(--color-text-muted); font-size: var(--font-size-xs); }

/* The sheet */
.xc-sheet { min-width: 0; }
.xc-back { display: none; }
.xc-sheet-head { display: flex; align-items: baseline; gap: var(--space-sm); }
.xc-sheet-head h2 { margin: 0; font-size: 1.125rem; }
.xc-sheet-head .xc-code { font-size: var(--font-size-md); }
.xc-sheet-meaning { max-width: 62ch; margin: 2px 0 0; color: var(--color-text-secondary); font-size: var(--font-size-md); }
.xc-part { margin-top: var(--space-xl); }
.xc-part h3 { margin: 0 0 var(--space-xs); font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-secondary); }
.xc-h3-sugg { color: var(--color-accent); }
.xc-h3-flag { color: var(--color-status-warning); }
.xc-onfile { padding: var(--space-xs) 0; font-size: var(--font-size-md); font-variant-numeric: tabular-nums; }
.xc-onfile .xc-meta { margin-top: 2px; }
.xc-onfile.known-entry { padding-left: var(--space-sm); }
.xc-onfile + .xc-onfile, .xc-onfile + [data-sheet-landed] > .xc-onfile:first-child { border-top: 1px solid var(--color-border-subtle); }
.xc-add { margin-top: var(--space-sm); }
.xc-why-none { max-width: 62ch; margin: var(--space-xl) 0 0; color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.xc-sugg { padding: var(--space-md) var(--space-lg); border: 1px solid var(--color-border-subtle); border-left: 3px solid var(--color-accent); border-radius: 0 var(--radius-md) var(--radius-md) 0; background: var(--color-bg-surface); }
.xc-sugg.is-unrecorded { border-left-color: var(--color-status-warning); }
.xc-sugg-value { margin: 0; font-size: 1.25rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.xc-sugg-value .xc-when { margin-left: var(--space-xs); font-size: var(--font-size-md); font-weight: 400; }
.xc-why { max-width: 62ch; margin: 0; color: var(--color-text-secondary); font-size: var(--font-size-sm); line-height: 1.45; }
.xc-link { padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
.xc-link:hover { color: var(--color-text-primary); }
.xc-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.xc-stories { margin: var(--space-sm) 0 0; padding: 0 0 0 var(--space-sm); list-style: none; border-left: 1px solid var(--color-border-subtle); }
.xc-stories[hidden] { display: none; }
.xc-stories li { display: grid; grid-template-columns: 3.5rem 1fr; gap: var(--space-sm); font-size: var(--font-size-sm); line-height: 1.45; }
.xc-stories li + li { margin-top: var(--space-xs); }
.xc-stories a { font-family: var(--font-mono); font-size: var(--font-size-xs); color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
.xc-stories-more { color: var(--color-text-muted); font-size: var(--font-size-xs) !important; }
.xc-caveat { max-width: 62ch; margin: var(--space-sm) 0 0; color: var(--color-text-muted); font-size: var(--font-size-xs); }
.xc-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-top: var(--space-md); }
.xc-actions[hidden] { display: none; }
.xc-actions .btn-small { padding: 4px 12px; font-size: var(--font-size-sm); }
.xc-go.btn-small { border-color: var(--color-accent); background: var(--color-accent); color: var(--color-text-on-accent); font-weight: 500; }
.xc-go.btn-small:hover { border-color: var(--color-accent-hover); background: var(--color-accent-hover); }
.xc-quiet.btn-small { padding-left: 4px; padding-right: 4px; border-color: transparent; background: none; color: var(--color-text-secondary); text-decoration: underline; text-underline-offset: 2px; }
.xc-quiet.btn-small:hover { background: none; color: var(--color-text-primary); }
.xc-outcome { margin: var(--space-md) 0 0; color: var(--color-status-resolved); font-size: var(--font-size-sm); }
.xc-outcome[hidden] { display: none; }
.xc-outcome.is-refused { color: var(--color-status-fault); }
.xc-offer.is-settled .xb, .xc-offer.is-settled .xb-key, .xc-offer.is-settled .xc-sugg-value { opacity: 0.55; }

/* Change, in place (IW-830): the numbers and when the value applies, with the
   chips the row form uses. */
.xc-change { margin-top: var(--space-md); padding-top: var(--space-md); border-top: 1px dashed var(--color-border-subtle); }
.xc-change[hidden] { display: none; }
.xc-fields { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-sm); }
.xc-field { display: grid; gap: 2px; color: var(--color-text-muted); font-size: var(--font-size-xs); }
.xc-field input { width: 7rem; padding: 5px 8px; font-size: var(--font-size-md); font-variant-numeric: tabular-nums; }
.xc-unit { padding-bottom: 7px; color: var(--color-text-muted); font-size: var(--font-size-sm); }
.xc-applies { margin: var(--space-md) 0 0; padding: 0; border: 0; }
.xc-applies legend { padding: 0; color: var(--color-text-muted); font-size: var(--font-size-xs); }
.xc-applies .xf-chips { margin-top: var(--space-xs); }
.xc-applies .xc-meta { margin-top: var(--space-xs); }

.xc-next { margin: var(--space-xl) 0 0; padding-top: var(--space-md); border-top: 1px solid var(--color-border-subtle); color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.xc-next a { color: var(--color-text-primary); }

/* The band */
.xb {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: 0 var(--space-xs);
  max-width: 36rem;
  margin: var(--space-md) 0 var(--space-xs);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.xb-min, .xb-max { padding-top: 8px; white-space: nowrap; }
.xb-min { text-align: right; }
.xb-track { position: relative; height: 34px; }
.xb-axis { position: absolute; left: 0; right: 0; top: 13px; height: 1px; background: var(--color-text-muted); opacity: 0.55; }
.xb-file, .xb-known { position: absolute; top: 8px; height: 11px; border-radius: 2px; }
.xb-file { background: var(--color-text-muted); opacity: 0.4; }
.xb-known { background: var(--color-status-warning); opacity: 0.55; }
.xb-sugg {
  position: absolute;
  top: 6px;
  height: 15px;
  box-sizing: border-box;
  border: 2px solid var(--color-accent);
  border-radius: 3px;
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}
.xb-sugg.is-unrecorded { border-color: var(--color-status-warning); background: color-mix(in srgb, var(--color-status-warning) 12%, transparent); }
.xb-tick { position: absolute; top: 23px; width: 2px; height: 9px; margin-left: -1px; border-radius: 1px; background: var(--color-text-primary); opacity: 0.8; }
.xb-key { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-lg); margin: 0 0 var(--space-md); color: var(--color-text-muted); font-size: var(--font-size-xs); }
.xb-key span { display: inline-flex; align-items: center; gap: 6px; }
.xb-key i { display: inline-block; height: 8px; border-radius: 2px; }
.xb-k-sugg { width: 16px; height: 10px !important; box-sizing: border-box; border: 2px solid var(--color-accent); background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.xb-k-sugg.is-unrecorded { border-color: var(--color-status-warning); }
.xb-k-file { width: 16px; background: var(--color-text-muted); opacity: 0.4; }
.xb-k-known { width: 16px; background: var(--color-status-warning); opacity: 0.55; }
.xb-k-tick { width: 2px; background: var(--color-text-primary); opacity: 0.8; }

/* A phone: the list, or the sheet with a way back to it. With no reading named
   the list shows; a reading opened from it shows its sheet. */
@container pane (max-width: 720px) {
  .xc-split { grid-template-columns: 1fr; }
  .xc-list { position: static; max-height: none; overflow: visible; }
  .xc-split:not(.is-resting) .xc-list { display: none; }
  .xc-split.is-resting .xc-sheet { display: none; }
  .xc-back { display: inline-block; margin-bottom: var(--space-sm); color: var(--color-text-secondary); font-size: var(--font-size-sm); }
  .xc-back::before { content: "‹ "; }
  .xc-sugg { padding: var(--space-md); }
  .xc-actions .btn-small { flex: 1 1 auto; padding: 8px 12px; text-align: center; }
  .xc-stories li { grid-template-columns: 1fr; gap: 0; }
}

/* ════════════════════════════════════════════════════════════════════
 * Facts about boats as survey sheets (IW-828)
 * ════════════════════════════════════════════════════════════════════
 * The page this block draws is Facts about boats alone: every rule here sits
 * under its page or its sheet, and the cards it restyles keep their own look
 * on the ticket screen and the boat page. Each boat is one sheet with a name
 * plate; the boat is named there and nowhere else on the sheet. Under the
 * plate come the device checks, each a block reading question, rows,
 * footer, and then the short facts as aligned rows: what, the value with its
 * evidence, and the two buttons. The three device choices are said once in
 * the page's key line, so a check drops its own copy. The page widens to the
 * pane, because a device row holds its photo, its reading and its choices on
 * one line on a desktop.
 */
.learnings-page[data-queue="facts"] { max-width: 60rem; }
.learnings-page[data-queue="facts"] .learnings-section,
.learnings-page[data-queue="facts"] .proposal-card { max-width: none; }
.learnings-page[data-queue="facts"] .learnings-lede { max-width: 62ch; font-size: var(--font-size-prose); }
.learnings-key {
  max-width: 72ch;
  margin: var(--space-sm) 0;
  font-size: var(--font-size-sm);
  line-height: 1.55;
  color: var(--color-text-muted);
}
.learnings-key strong { font-weight: 600; color: var(--color-text-secondary); }
.fx-sheet {
  margin: 0 0 var(--space-xl);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}
.fx-plate {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-md);
  padding: var(--space-lg) var(--space-xl);
  border-bottom: 1px solid var(--color-border-subtle);
}
.fx-sheet .rq-boat { margin: 0; font-size: var(--font-size-title); }
.fx-sheet .rq-boat a { color: var(--color-text-primary); text-decoration: none; }
.fx-sheet .rq-boat a:hover { text-decoration: underline; text-underline-offset: 3px; }
.fx-owner { margin: 0; font-size: var(--font-size-md); color: var(--color-text-muted); }
.fx-plate .reg-key { font-size: var(--font-size-xs); }
.fx-block { padding: var(--space-lg) var(--space-xl); }
.fx-block + .fx-block { border-top: 1px solid var(--color-border-subtle); }

/* A device check: the chat's own card, laid flat into its block. The tag that
   names the boat goes, the plate has said it; the claim is the block's
   question; the ticket it was raised on and the buttons share the footer. */
.fx-block > .proposal-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-md);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-size: var(--font-size-md);
}
.fx-block > .proposal-card > * { flex: 1 1 100%; min-width: 0; }
.fx-block > .proposal-card > .proposal-tag { display: none; }
.fx-block > .proposal-card > .proposal-claim { margin: 0; font-weight: 600; }
.fx-block > .proposal-card > .proposal-trigger { margin-top: var(--space-xs); max-width: 64ch; color: var(--color-text-secondary); line-height: 1.55; }
.fx-block > .proposal-card > .proposal-devices { margin-top: var(--space-md); }
.fx-block > .proposal-card .proposal-devices > .device-help:not(.device-unmarked-note),
.fx-block > .proposal-card .device-choices > .device-help:not(.device-unmarked-note) { display: none; }
.fx-block > .proposal-card > .proposal-effect {
  order: 1;
  margin-top: var(--space-md);
  padding-top: 0;
  border-top: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.fx-block > .proposal-card > .proposal-source {
  order: 2;
  flex: 1 1 auto;
  margin-top: var(--space-md);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.fx-block > .proposal-card > .proposal-actions {
  order: 3;
  flex: 0 0 auto;
  margin-top: var(--space-md);
  padding-top: 0;
  border-top: 0;
}
.fx-block .proposal-devices { margin-left: 0; margin-right: 0; padding: 0; border: 0; min-width: 0; }
.fx-block .device-group { margin-top: var(--space-sm); padding: 0; }
.fx-block .device-group-head { font-weight: 600; color: var(--color-text-secondary); }
/* The check's question heads it (learnings._on_sheet), so the block's own
   copy of it stays only as the choices' legend for a screen reader: the
   devices' lead, or on a check of serial numbers alone, the serials' head. */
.fx-block .proposal-devices > legend.proposal-people-lead,
.fx-block .proposal-devices:not(:has(.device-row)) > .device-serials > .device-group-head {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* A device row: the photos it was read from, what it is, and its three
   choices, on one line between rules. A row with no photo draws no empty
   column for one. */
.fx-block .device-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  padding: var(--space-md) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.fx-block .device-row:has(> .device-row-photos) { grid-template-columns: auto minmax(0, 1fr) auto; }
/* Once any row in a group has a photo, every row keeps the photo column, so
   the readings line up down the group. */
.fx-block .device-group:has(.device-row-photos) > .device-row { grid-template-columns: 88px minmax(0, 1fr) auto; }
.fx-block .device-group:has(.device-row-photos) > .device-row > .device-who { grid-column: 2; }
.fx-block .device-group:has(.device-row-photos) > .device-row > .device-state { grid-column: 3; }
.fx-block .device-row + .device-row { border-top-style: solid; }
/* A device row's photos sit side by side, at the 4:3 a phone takes them in,
   large enough to make out a screen's columns before opening one. */
.fx-block .device-row-photos { flex-direction: row; width: auto; }
.fx-block .device-row-photos .device-thumb { width: 88px; height: 66px; }
.fx-block .device-row > .device-row-photos { grid-column: 1; flex-wrap: wrap; max-width: 88px; }
.fx-block .device-row > .device-photo { grid-column: 1 / -1; }
.fx-block .device-row > :not(.device-row-photos):not(.device-who):not(.device-state) { grid-column: 1 / -1; }
.fx-block .device-name { font-weight: 500; }

/* A short fact: the registry card as one row of three columns, the type, the
   value with where it came from, and its buttons. The boat it names is the
   sheet's, so the card's own copy of the name goes. */
.fx-facts-head {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-secondary);
}
/* Details goes with its last fact: a settled card leaves the page, and a
   check still waiting keeps the sheet. */
.fx-facts:not(:has([data-proposal-key])) { display: none; }
.fx-facts > .rq-card {
  display: grid;
  grid-template-columns: 11rem minmax(0, 1fr) auto;
  column-gap: var(--space-md);
  align-items: start;
  margin: 0;
  padding: var(--space-md) 0;
  border: 0;
  border-top: 1px solid var(--color-border-subtle);
  border-radius: 0;
  background: none;
  font-size: var(--font-size-md);
}
.fx-facts > .rq-card > * { grid-column: 2; margin: 0; }
.fx-facts > .rq-card > .proposal-line { display: contents; }
/* The type stays the row's only name for the fact, in the chip's own family
   colour, but in the sans face at the row's small size: in a column of rows it
   is read as a label beside the value, not as a title over it. */
.fx-facts > .rq-card .rq-type {
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-weight: 500;
}
.fx-facts > .rq-card .rq-card-boat { display: none; }
.fx-facts > .rq-card .proposal-value,
.fx-facts > .rq-card .rq-address-toggle { grid-column: 2; grid-row: 1; justify-self: start; font-weight: 500; color: var(--color-text-primary); }
.fx-facts > .rq-card .proposal-quote { margin: var(--space-sm) 0 var(--space-xs); }
.fx-facts > .rq-card .proposal-trigger { font-size: var(--font-size-sm); color: var(--color-text-muted); }
.fx-facts > .rq-card .rq-country { margin-top: var(--space-sm); }
.fx-facts > .rq-card > .proposal-actions {
  grid-column: 3;
  grid-row: 1 / span 6;
  margin: 0;
  padding: 0;
  border: 0;
}

/* A pane too narrow for a row's three parts on one line (a phone, or a laptop
   with the rail open wide) puts the choices under the reading. */
@container pane (max-width: 720px) {
  .fx-plate, .fx-block { padding-left: var(--space-lg); padding-right: var(--space-lg); }
  .fx-block .device-row { grid-template-columns: minmax(0, 1fr); }
  .fx-block .device-row:has(> .device-row-photos),
  .fx-block .device-group:has(.device-row-photos) > .device-row { grid-template-columns: 64px minmax(0, 1fr); align-items: start; }
  .fx-block .device-row-photos .device-thumb { width: 64px; height: 48px; }
  .fx-block .device-row > .device-row-photos { max-width: 64px; }
  .fx-block .device-row > .device-state,
  .fx-block .device-group:has(.device-row-photos) > .device-row > .device-state { grid-column: 1 / -1; display: flex; }
  .fx-block .device-row > .device-state .device-state-option {
    flex: 1 1 0;
    justify-content: center;
    padding: var(--space-xs);
    text-align: center;
    white-space: normal;
  }
  .fx-facts > .rq-card { grid-template-columns: minmax(0, 1fr); row-gap: var(--space-xs); }
  .fx-facts > .rq-card > *,
  .fx-facts > .rq-card .rq-type,
  .fx-facts > .rq-card .proposal-value,
  .fx-facts > .rq-card .rq-address-toggle,
  .fx-facts > .rq-card > .proposal-actions { grid-column: 1; grid-row: auto; }
  .fx-facts > .rq-card > .proposal-actions { margin-top: var(--space-sm); }
}

/* ════════════════════════════════════════════════════════════════════
 * Lessons from past tickets: the guided review (IW-826)
 * ════════════════════════════════════════════════════════════════════
 * Folded in from the mockup's lessons.css (lessons-review-mockup, 23 October
 * 2026) as its own block, by Phil's rule for the shared stylesheet: it uses
 * only the semantic tokens and changes no shared colour, spacing or type token.
 *
 * A lesson is a claim drawn from several resolved tickets, put to one support
 * person at a time. The page is built so the reviewer judges it rather than
 * waves it through: the claim is the largest text, what it rests on is shown
 * with how each piece was known, and the evidence line puts every ticket
 * behind it on one time axis, so the age of the lesson and any newer ticket
 * that disagrees are seen before the answer.
 *
 * The evidence line is where this screen spends its boldness. Everything else
 * is the Learnings page's own register: hairlines, the 60ch measure, the
 * cards' type size. The accent appears on the evidence ticks and the one
 * button that records an answer, nothing else. A newer ticket that disagrees
 * takes the warning tone, never the fault tone: it is a reason to look, not
 * something broken. */

/* The review is the queue's section (.learnings-section), at the mockup's own
   column rather than the cards' measure. */
.lesson-page.learnings-section { max-width: 46rem; margin-top: 0; }
.lesson-page .learnings-lede,
.lesson-page .rq-pii-note { max-width: 60ch; }

/* Where the session stands: five short segments, one per lesson, since the session is
   a real sequence. The words beside them say the same thing for a screen reader. */
.lesson-progress {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin: var(--space-lg) 0 var(--space-md);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.lesson-progress-bar { display: flex; gap: 3px; }
.lesson-progress-bar span {
  width: 22px;
  height: 3px;
  border-radius: 2px;
  background: var(--color-border-subtle);
}
.lesson-progress-bar .is-done { background: var(--color-text-muted); }
.lesson-progress-bar .is-current { background: var(--color-accent); }

/* The case file. One column at the cards' measure, sections divided by hairlines
   rather than boxed, so it reads as one document and not a stack of cards. */
.lesson-file {
  max-width: 60ch;
  font-size: var(--font-size-md);
  border-top: 1px solid var(--color-border-subtle);
}
.lesson-claim {
  font-size: var(--font-size-claim);
  font-weight: 500;
  line-height: 1.45;
  color: var(--color-text-primary);
  margin: var(--space-lg) 0 var(--space-sm);
}
.lesson-basis {
  margin: 0 0 var(--space-lg);
  color: var(--color-text-secondary);
}
.lesson-part {
  border-top: 1px solid var(--color-border-subtle);
  padding: var(--space-md) 0 var(--space-lg);
}
.lesson-part-head {
  font-size: var(--font-size-md);
  font-weight: 600;
  margin: 0 0 var(--space-sm);
  color: var(--color-text-primary);
}

/* What the lesson rests on. A two column list: the thing, then its value and how it
   was known. How it was known takes the confidence tones the brief already uses, so a
   value read in the ticket and a value nobody could find read differently at once. */
.lesson-rests {
  display: grid;
  grid-template-columns: minmax(7rem, max-content) 1fr;
  gap: var(--space-xs) var(--space-lg);
  margin: 0;
}
.lesson-rests dt { color: var(--color-text-muted); }
.lesson-rests dd { margin: 0; color: var(--color-text-primary); }
.lesson-known {
  margin-left: var(--space-sm);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}
.lesson-known-stated { color: var(--color-confidence-high); }
.lesson-known-inferred { color: var(--color-confidence-middle); }
.lesson-known-unknown { color: var(--color-confidence-low); }
.lesson-weak {
  margin: var(--space-sm) 0 0;
  color: var(--color-confidence-low);
  font-size: var(--font-size-sm);
}

/* The evidence line: January 2023 to today on one axis. Each ticket behind the lesson
   is a solid tick; a newer ticket that disagrees is a hollow marker in the warning
   tone, with its label hung below the line. Positions are percentages of the span,
   set inline by the server, so the line needs no script. */
.evidence-line {
  position: relative;
  height: 64px;
  margin: var(--space-md) 0 var(--space-xs);
}
.evidence-axis {
  position: absolute;
  left: 0;
  right: 0;
  top: 28px;
  height: 1px;
  background: var(--color-ledger-rail);
}
.evidence-year {
  position: absolute;
  top: 38px;
  transform: translateX(-50%);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.evidence-year::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -10px;
  width: 1px;
  height: 5px;
  background: var(--color-ledger-rail);
}
.evidence-year-first { transform: none; }
.evidence-year-first::before { left: 0; }
.evidence-year-now { transform: translateX(-100%); }
.evidence-year-now::before { left: auto; right: 0; }
.evidence-tick {
  position: absolute;
  top: 14px;
  width: 3px;
  height: 14px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--color-ledger-tick);
}
.evidence-newer {
  position: absolute;
  top: 21px;
  width: 13px;
  height: 13px;
  margin-left: -6px;
  border: 2px solid var(--color-status-warning);
  border-radius: 50%;
  background: var(--color-bg-page);
}
.evidence-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.evidence-key-tick::before,
.evidence-key-newer::before {
  content: "";
  display: inline-block;
  vertical-align: -1px;
  margin-right: var(--space-xs);
}
.evidence-key-tick::before {
  width: 3px;
  height: 11px;
  background: var(--color-ledger-tick);
}
.evidence-key-newer::before {
  width: 9px;
  height: 9px;
  border: 2px solid var(--color-status-warning);
  border-radius: 50%;
}

/* The tickets behind it: one ledger row each, date and number first as data, then the
   ticket's story in one line. Read on demand, so the list is a disclosure that opens
   when there are three or fewer. */
.lesson-page a { color: var(--color-accent); }
.lesson-page a:hover { color: var(--color-accent-hover); }
/* The product's disclosure register is a boxed, tracked label for panels of data. This
   one opens a short reading list, so it is a plain line of text with its marker. */
.lesson-page .lesson-tickets { margin: var(--space-md) 0 0; border: 0; background: none; padding: 0; }
.lesson-page .lesson-tickets > summary {
  cursor: pointer;
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--font-size-md);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-text-secondary);
}
.lesson-ticket-list { list-style: none; margin: var(--space-sm) 0 0; padding: 0; }
.lesson-ticket {
  display: grid;
  grid-template-columns: 6.5rem 1fr;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.lesson-ticket-when {
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}
.lesson-ticket-when a { display: block; }
.lesson-ticket-story { color: var(--color-text-primary); }

/* A newer ticket that disagrees. Set beside the lesson's own fix, the older above the
   newer, so the reviewer compares the two fixes and not two paragraphs. */
.lesson-newer {
  border-left: 3px solid var(--color-status-warning);
  background: var(--color-status-warning-bg);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: var(--space-md) var(--space-lg);
}
.lesson-newer .lesson-part-head { color: var(--color-status-warning); }
.lesson-compare {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: var(--space-xs) var(--space-md);
  margin: 0;
}
.lesson-compare dt { color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
.lesson-compare dd { margin: 0; color: var(--color-text-primary); }
.lesson-newer-note { margin: var(--space-sm) 0 0; color: var(--color-text-secondary); }

/* The answer. Four choices, each a full row with what it does in one line, never a
   bare yes. A choice that needs more (narrowing, the newer fix, who to ask) opens its
   field under it, inside the choice's box but outside its label, so a click in the
   field never lands on the radio. The label spans the box's width on the box's own
   columns (a subgrid), and carries the box's padding, so a click anywhere on the row picks it. The button that records stays disabled until a choice is made. */
.lesson-answer { border: 0; margin: 0; padding: 0; }
.lesson-answer legend {
  padding: 0;
  font-weight: 600;
  margin-bottom: var(--space-sm);
}
.lesson-choice {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--space-sm);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-bg-surface);
}
.lesson-choice-pick {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  row-gap: 2px;
  padding: var(--space-sm) var(--space-md);
  cursor: pointer;
}
.lesson-choice + .lesson-choice { margin-top: var(--space-xs); }
.lesson-choice input { margin: 3px 0 0; accent-color: var(--color-accent); }
.lesson-choice-name { font-weight: 500; color: var(--color-text-primary); }
.lesson-choice-does { grid-column: 2; color: var(--color-text-muted); font-size: var(--font-size-sm); }
.lesson-choice:has(input:checked) {
  border-color: var(--color-accent);
  background: var(--color-bg-selected);
}
.lesson-choice-more { grid-column: 2; padding: 0 var(--space-md) var(--space-sm) 0; }
.lesson-more-label {
  display: block;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-xs);
}
.lesson-choice-more textarea,
.lesson-choice-more select {
  width: 100%;
  font: inherit;
  color: var(--color-text-primary);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: var(--space-xs) var(--space-sm);
}
.lesson-choice-more select + .lesson-more-label { margin-top: var(--space-sm); }
.lesson-choice-more textarea { min-height: 6em; field-sizing: content; resize: vertical; line-height: 1.5; }
.lesson-actions {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  margin-top: var(--space-lg);
}
.lesson-skip { color: var(--color-text-muted); font-size: var(--font-size-sm); }

/* The recap that ends a session: what this person settled, one hairline row each,
   then what earlier answers have done since. */
.lesson-recap { max-width: 60ch; font-size: var(--font-size-md); }
.lesson-recap-head {
  font-size: var(--font-size-claim);
  font-weight: 500;
  margin: var(--space-lg) 0 var(--space-xs);
}
.lesson-recap-list { list-style: none; margin: var(--space-md) 0; padding: 0; }
.lesson-recap-list li {
  display: grid;
  grid-template-columns: 9.5rem 1fr;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--color-border-subtle);
}
.lesson-recap-answer { color: var(--color-text-secondary); }
.lesson-recap-answer-outdated { color: var(--color-status-warning); }
.lesson-recap-answer-holds { color: var(--color-status-resolved); }
.lesson-helped {
  border-top: 1px solid var(--color-border-subtle);
  padding-top: var(--space-md);
}
.lesson-helped-list { margin: var(--space-sm) 0 0; padding-left: var(--space-lg); color: var(--color-text-secondary); }
.lesson-helped-list li + li { margin-top: var(--space-xs); }

/* A narrow pane (a phone, or a laptop with the rail open wide), on the pane's
   tight step like every screen: the two column rows stack their label over their value. The evidence
   line keeps its axis; it fits the narrowest pane at any position. */
@container pane (max-width: 560px) {
  .lesson-rests,
  .lesson-ticket,
  .lesson-compare,
  .lesson-recap-list li { grid-template-columns: 1fr; gap: 0 var(--space-md); }
  .lesson-rests dd + dt { margin-top: var(--space-sm); }
  .lesson-compare dd + dt { margin-top: var(--space-sm); }
  .lesson-ticket-when a { display: inline; margin-left: var(--space-sm); }
  .lesson-actions { flex-wrap: wrap; }
}

/* What the build adds to the mockup: the line saying a teammate asked you, a
   refusal under the choices, a ticket number with no Freshdesk link, and the
   recap's Start another five, a GET form so it starts a fresh session. */
.lesson-asked {
  margin: var(--space-lg) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}
.lesson-asked + .lesson-claim { margin-top: var(--space-xs); }
.lesson-error { margin: var(--space-sm) 0 0; color: var(--color-status-fault); }
.lesson-ticket-number { display: block; }
.lesson-actions form { margin: 0; }
@container pane (max-width: 560px) {
  .lesson-ticket-number { display: inline; margin-left: var(--space-sm); }
}

/* ── Add a new partner company (IW-860) ─────────────────────────────────
 * The short form a partner list opens under itself when its last choice,
 * Add a new partner company, is picked, and the Partner companies page's own
 * add form. It wears the partner entry's accent rule, so it reads as the
 * entry being made, beside the list it will join; everything else in it is
 * the quiet form register. Its location fields are the company page's own
 * (_partner_location_fields.html), laid out here as a pair. */
.partner-add {
  display: grid;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  padding: var(--space-xs) 0 var(--space-xs) var(--space-md);
  border-left: 3px solid var(--color-accent);
  max-width: 34rem;
  font-size: var(--font-size-sm);
}
.partner-add[hidden] { display: none; }
/* On the Partner companies page the form stands alone above the list, so it
 * takes the form box every Boats page form wears, keeping its accent rule,
 * and does not read as one more entry in the list under it. */
.partner-add[data-partner-add-page] {
  padding: var(--space-sm) var(--space-md);
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-sm);
}
.partner-add-title { margin: 0; font-weight: 600; color: var(--color-text-primary); }
.partner-add-fields,
.partner-add-place {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: end;
  gap: var(--space-sm);
  min-width: 0;
}
.partner-add-fields .partner-add-wide { grid-column: 1 / -1; }
.partner-add input,
.partner-add select { width: 100%; box-sizing: border-box; }
.partner-add-hint { color: var(--color-text-muted); font-size: var(--font-size-xs); }
.partner-add-legend {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--color-text-primary);
  font-weight: 600;
}
/* A location field's label sits on its control, as .rq-company-field's do. */
.partner-add .partner-loc-field {
  display: grid;
  gap: 2px;
  min-width: 0;
  color: var(--color-text-secondary);
}
.partner-add-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
}
/* .btn-link is coloured for the inverse header; Cancel sits on a card. */
.partner-add .btn-link { color: var(--color-text-secondary); opacity: 1; }
.partner-add .btn-link:hover { color: var(--color-text-primary); }
.partner-add-status { color: var(--color-text-secondary); }
/* What was added, said under the list once the form shuts. */
.partner-add-done {
  flex-basis: 100%;
  margin: var(--space-xs) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}
.partner-add-fold { margin: var(--space-md) 0; }
/* Inside the people block the form sits in the dropdowns' column, which is
 * narrow on a wide screen too (about 14rem on a New boat card), so there it
 * stacks every field, as the requester's own New partner company form does. */
.rq-people .partner-add-fields,
.rq-people .partner-add-place { grid-template-columns: minmax(0, 1fr); }
@container pane (max-width: 560px) {
  .partner-add-fields,
  .partner-add-place { grid-template-columns: minmax(0, 1fr); }
}

/* ── Place suggestions for a partner location (IW-861) ──────────────────
 * A partner company location's place box suggests places as the support
 * person types; picking one fills the place, its town and region, and the
 * country list beside it. The list floats under the box, over the fields
 * below, in the raised panel the rail's search results wear, so the form
 * never jumps as it comes and goes. Each suggestion reads its name first,
 * in the primary tone, then where it is in the secondary tone, so a town
 * and a marina in it are told apart at a glance. The suggestion the arrow
 * keys rest on carries the partner entry's accent rule. The data licence's
 * credit closes the list, quiet. */
.partner-loc-place {
  position: relative;
  display: grid;
  gap: 2px;
  min-width: 0;
}
.partner-loc-place .partner-loc-name { width: 100%; box-sizing: border-box; }
.partner-loc-picked {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
}
.partner-loc-picked:empty { display: none; }
.place-suggest {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 30;
  /* At least the box's width, and wider when a place's name and where it is
   * need it, so a suggestion reads on one line beside a half width box. */
  min-width: 100%;
  width: max-content;
  max-width: 28rem;
  margin-top: 2px;
  padding: var(--space-xs) 0;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
  font-size: var(--font-size-sm);
}
.place-suggest[hidden] { display: none; }
/* In a narrow pane or the people block's narrow column the list keeps to the
 * box's width and its suggestions wrap, so it never runs past the edge. */
.rq-people .place-suggest { width: auto; max-width: none; right: 0; }
@container pane (max-width: 560px) {
  .place-suggest { width: auto; max-width: none; right: 0; }
}
.place-suggest-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 16rem;
  overflow-y: auto;
}
.place-suggest-option {
  padding: var(--space-xs) var(--space-md);
  color: var(--color-text-primary);
  line-height: 1.35;
  cursor: pointer;
}
.place-suggest-option:hover { background: var(--color-bg-hover); }
.place-suggest-option[aria-selected="true"] {
  background: var(--color-bg-hover);
  box-shadow: inset 3px 0 0 var(--color-accent);
}
.place-suggest-name { font-weight: 600; }
.place-suggest-where { margin-left: var(--space-xs); color: var(--color-text-secondary); }
.place-suggest-credit {
  margin: var(--space-xs) var(--space-md) 0;
  padding-top: var(--space-xs);
  border-top: 1px solid var(--color-border-subtle);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
/* In the add partner company form the place and its country share a row and
 * the street address takes the row under them. The place's label can gain
 * the picked line under its box, so the row lines up at the top. */
.partner-add .partner-add-place { align-items: start; }
.partner-add .partner-loc-address-field { grid-column: 1 / -1; }
@media (forced-colors: active) {
  .place-suggest-option[aria-selected="true"] { outline: 2px solid Highlight; outline-offset: -2px; }
}

/* ── A partner company on file with the typed name (IW-871) ──────────────
 * Said inside the add form, above its buttons, in the warning tone a Names
 * to review row uses for a boat on file with the same name: it is a
 * question to settle before adding, not a fault. Each company on file is a
 * line of its own with its offer beside it, the form's primary action now,
 * while Add company steps down to the secondary "Add as a different
 * company". */
.partner-add-match {
  display: grid;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-status-warning-bg);
  border-left: 3px solid var(--color-status-warning);
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
}
.partner-add-match-title { margin: 0; font-weight: 600; }
.partner-add-match-list {
  display: grid;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}
.partner-add-match-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-md);
  min-width: 0;
}
/* Two or more companies on file are told apart by a hairline, so a narrow
 * column, where each offer wraps under its name, still reads as one per line. */
.partner-add-match-list li + li {
  padding-top: var(--space-xs);
  border-top: 1px solid var(--color-border-subtle);
}
.partner-add-match-name { flex: 1 1 14rem; min-width: 0; overflow-wrap: anywhere; }
.partner-add-match-why,
.partner-add-match-hint { margin: 0; color: var(--color-text-secondary); }
.partner-add-match-why { flex: 1 1 100%; font-size: var(--font-size-xs); }
.partner-add-match .btn-primary,
.partner-add-match .btn-secondary { text-decoration: none; }

/* ── Hand back the No Foreign Land page, or say it is not there (IW-866) ─────
 * With no link on file, a support person searches in the next tab and comes
 * back with an address, or with nothing. Three moves, in the order they make
 * them: the searches (the boat's name, and the owner's when one is on file),
 * the paste slot with Save, and "Not on No Foreign Land". The boat page row
 * shows them inline; the plate and the ticket header put them in one small
 * panel behind a quiet disclosure, so the header line keeps its height.
 * The paste slot is the one thing given weight: a mono field (the product sets
 * addresses as technical data) with the dashed edge the plate's Fill in uses
 * for something to fill in, solid accent once focused. Everything around it
 * stays quiet; the mark is said in the secondary ink, never a status colour. */
.nfl-find-searches {
  display: flex;
  flex-wrap: wrap;
  gap: 2px var(--space-md);
}
.nfl-find-search {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
  text-decoration: none;
}
.nfl-find-search:hover { color: var(--color-accent); text-decoration: underline; }
.nfl-find-search[hidden] { display: none; }
/* The two forms ride .reg-form on the boat page for its submit wiring, so the
 * box that class draws is taken off here; they are a line, not a card. */
.nfl-find .reg-form,
.nfl-find .nfl-find-paste,
.nfl-find .nfl-find-mark {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  /* .reg-form grows to 280px along its parent's axis; stacked in a column,
   * that would be a tall empty band, so these keep their own height. */
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--font-size-sm);
}
.nfl-find .nfl-find-paste[hidden],
.nfl-find .nfl-find-mark[hidden] { display: none; }
.nfl-find input.nfl-find-input {
  flex: 1 1 14rem;
  width: auto;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  border: 1px dashed var(--color-text-muted);
  background: var(--color-bg-surface);
}
.nfl-find input.nfl-find-input::placeholder { font-family: var(--font-sans); color: var(--color-text-muted); }
.nfl-find input.nfl-find-input:focus {
  border: 1px solid var(--color-accent);
  outline: 1px solid var(--color-accent);
  outline-offset: 0;
}
.nfl-find .nfl-find-save { flex: none; padding: 6px var(--space-lg); }
/* Not on No Foreign Land: a plain statement in the secondary ink with a
 * hairline under it, so it reads as a choice to make and never as a link out. */
.nfl-find-not-on {
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-border-subtle);
  border-radius: 0;
  padding: 0 0 1px;
  cursor: pointer;
}
.nfl-find-not-on:hover { color: var(--color-text-primary); border-bottom-color: var(--color-text-secondary); }
.nfl-find-not-on:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.nfl-find-status {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
.nfl-find-status:empty { display: none; }

/* The boat page row: the moves stacked under "no link on file", in the
 * value's column, at the width of an address. */
.nfl-find-inline {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-sm);
  max-width: 30rem;
  margin: var(--space-xs) 0 var(--space-sm);
}
/* The mark, where a link would be: the value's size, secondary ink. */
.nfl-not-on { color: var(--color-text-secondary); }

/* The disclosure the plate and the ticket header open. The summary keeps the
 * look of the link it stands in for; a small caret says it opens here rather
 * than in a new tab. The panel floats under it on the raised surface every
 * popover uses, and hairlines split it into its moves. */
.nfl-find-pop {
  position: relative;
  display: inline-block;
  /* Not a brief section: the page-wide details box is undone here. */
  margin: 0;
  border: 0;
  border-radius: 0;
}
.nfl-find-pop[hidden] { display: none; }
/* The page-wide summary register (padding, fill, bold capitals, triangle) is
 * taken off too; the caret is this disclosure's only marker. */
.nfl-find-pop > summary {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 0;
  background: none;
  border-radius: 0;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  list-style: none;
  cursor: pointer;
}
.nfl-find-pop > summary::-webkit-details-marker { display: none; }
.nfl-find-pop > summary::before,
.nfl-find-pop[open] > summary::before { content: none; }
.nfl-find-pop > summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.nfl-find-caret { width: 10px; height: 10px; flex: none; }
.nfl-find-pop[open] .nfl-find-caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: no-preference) {
  .nfl-find-caret { transition: transform 120ms ease; }
}
.nfl-find-panel {
  position: absolute;
  top: calc(100% + var(--space-xs));
  left: 0;
  z-index: 30;
  width: 21rem;
  max-width: calc(100vw - 2 * var(--space-lg));
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  color: var(--color-text-primary);
  text-align: left;
}
.nfl-find-title { margin: 0; font-size: var(--font-size-sm); font-weight: 600; }
.nfl-find-moves {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}
.nfl-find-moves[hidden] { display: none; }
.nfl-find-pop .nfl-find-paste,
.nfl-find-pop .nfl-find-mark {
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
}
.nfl-find-pop .nfl-find-searches { flex-direction: column; align-items: flex-start; gap: var(--space-xs); }
.nfl-find-pop .nfl-find-search { font-size: var(--font-size-sm); }
.nfl-find-marked {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}
.nfl-find-marked[hidden] { display: none; }
.nfl-find-who { margin: 0; color: var(--color-text-secondary); }
.nfl-find-undo { padding: 4px var(--space-md); }

/* On the plate the summary is the cell's small link; the panel opens from the
 * cell's left edge. A marked boat's words sit there unlinked, muted. */
.reg-plate-cell .nfl-find-pop > summary { display: inline; color: var(--color-text-secondary); }
.reg-plate-cell .nfl-find-caret { margin-left: 2px; vertical-align: middle; }
.reg-plate-cell .nfl-find-pop > summary:hover { color: var(--color-accent); }

/* In the ticket header "NFL" stays the header's mono label; a marked boat's
 * "Not on NFL" takes the muted tone, since there is nowhere to go. */
.boat-title-find > summary.boat-title-nfl {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
}
/* The page-wide rule that sets a summary's spans in plain type is undone, so
 * the words keep the header's capitals like FRESHDESK beside them. */
.boat-title-find > summary.boat-title-nfl span { text-transform: inherit; letter-spacing: inherit; }
.boat-title-find.is-marked > summary.boat-title-nfl { color: var(--color-text-muted); }
.boat-title-find > summary.boat-title-nfl:hover { color: var(--color-accent); }

/* ── Ready column on the Users table (IW-945) ────────────────────────────────
 * Whether each person can open a ticket today, read down one column during a
 * rollout. The breaker-panel lamp and word the credential rows use say it (live,
 * needs attention, down, never switched on), and under it each thing still
 * missing gets its own short line: what stops a ticket opening in the text
 * colour, what wants attention soon in the warning colour, and quiet notes (the
 * Jira connection, the last sign in) muted. Rows align to the top so every
 * person's name sits level with their lamp, however many lines follow. */
#admin-users .admin-table td { vertical-align: top; }
#admin-users td.admin-ready { min-width: 15ch; }
/* 36ch keeps the longest line, "Claude account token cannot be read", and a dated
 * one such as "Claude account token expires 12 Nov" whole on a desktop (IW-1021). */
.admin-ready-line {
  display: block;
  max-width: 36ch;
  margin-top: 3px;
  font-size: var(--font-size-xs);
  line-height: 1.45;
}
.admin-ready > span:first-child + .admin-ready-line { margin-top: var(--space-xs); }
.admin-ready-need { color: var(--color-text-primary); }
.admin-ready-warn { color: var(--color-status-warning); }
.admin-ready-note { color: var(--color-text-muted); }
/* On a phone the six columns cannot share the width, and the Ready column would
 * sit off screen behind a sideways scroll. Each person stacks instead: the name
 * with their status at its right, role and sign in beneath in the quiet register,
 * then the Ready readout, then the actions. */
@container pane (max-width: 560px) {
  #admin-users .admin-table thead { display: none; }
  #admin-users .admin-table,
  #admin-users .admin-table tbody { display: block; }
  #admin-users .admin-table tr {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "user status"
      "role role"
      "signin signin"
      "ready ready"
      "actions actions";
    padding: var(--space-md) 0;
    border-top: 1px solid var(--color-border-subtle);
  }
  #admin-users .admin-table td {
    padding: 0;
    border-top: none;
    text-align: left;
  }
  #admin-users .admin-table td:nth-child(1) { grid-area: user; }
  #admin-users .admin-table td:nth-child(2) { grid-area: role; margin-top: var(--space-xs); }
  #admin-users .admin-table td:nth-child(3) { grid-area: signin; margin-top: 2px; }
  #admin-users .admin-table td:nth-child(4) {
    grid-area: status;
    font-size: var(--font-size-xs);
    color: var(--color-text-secondary);
  }
  #admin-users .admin-table td.admin-ready { grid-area: ready; min-width: 0; margin-top: var(--space-sm); }
  #admin-users .admin-table td.admin-ready:empty { display: none; }
  #admin-users .admin-table td.admin-actions { grid-area: actions; margin-top: var(--space-sm); }
  .admin-ready-line { max-width: none; }
}
/* ── end of the Ready column (IW-945) ── */

/* ── A device detail the assistant saved on the boat (IW-913) ────────────────
 * The case note's ledger line (IW-801), since it is the same act: the assistant
 * wrote something, so the rail's tick marks it and Undo is the one control. Only
 * the words differ. A case note's value is cut to one line, but this line's
 * sentence is the whole of what was saved, a serial number included, so it wraps
 * rather than hiding the end of it. Undo and the state follow the last word. */
.boat-detail-note { flex-wrap: wrap; row-gap: 0; }
.boat-detail-note .case-note-text {
  flex: 0 1 auto;
  white-space: normal;
  overflow: visible;
}

/* ── Teams posts and Connect Microsoft (IW-949) ─────────────────────────────── */
/* A Teams post card is a draft card in every part but one: the line above the box
 * that says where the text will land and under whose name, because a post in a
 * boat's channel is read by the customer. The line is the card's address, set in
 * the secondary tone with the channel's name in the text tone and weight, so it is
 * the first thing read under the title and never louder than the text itself. */
.teams-post-to {
  margin: var(--space-xs) 0 0;
  max-width: var(--measure-prose);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}
.teams-post-to[hidden] { display: none; }
.teams-post-channel { font-weight: 600; color: var(--color-text-primary); }
a.teams-post-channel { color: var(--color-accent); }
.teams-post-card .draft-actions { flex-wrap: wrap; }
.teams-post-send[hidden] { display: none; }
/* The status line's way out (Open My settings, Open in Teams) is a link in the
 * accent, whatever tone the sentence before it takes. */
.teams-post-card .draft-status a { color: var(--color-accent); }
/* A posted card keeps its address in the muted tone and its box cannot be resized,
 * as its text is the record of what went out. */
.teams-post-card.draft-posted .teams-post-to { color: var(--color-text-muted); }
.teams-post-card.draft-posted .draft-edit { resize: none; }

/* The Microsoft connection row on My settings: the Jira row's shape, so the two
 * connections read as a pair. The status cell is capped and the date stacks under
 * the account for the same reason the Jira row gives. */
.microsoft-connect-actions {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
}
.microsoft-connect-actions a { text-decoration: none; display: inline-block; line-height: 1.3; }
.microsoft-connect-actions a.btn-secondary,
.microsoft-connect-actions a.btn-primary { padding: 6px var(--space-lg); }
.microsoft-connection-row td:nth-child(2) { max-width: 30ch; }
.microsoft-connection-row .cred-when,
.microsoft-connection-row .cred-detail { display: block; margin: var(--space-xs) 0 0; }
/* The table folds to stacked cells in a narrow pane, as the Jira row does. */
@container pane (max-width: 560px) {
  #my-credentials .microsoft-connect-actions { justify-content: flex-start; }
  #my-credentials .microsoft-connection-row td:nth-child(2) { max-width: none; }
}

/* The admin block's three delegated permissions use the Microsoft 365 panel's list,
 * whose tight pane drops what each is for under its name and button. These lines are
 * longer than that panel's own, and a line spanning both columns would size them by
 * its unwrapped width, pushing the Copy buttons past a phone's edge. So the line takes
 * no width of its own and wraps in the row, and the name may break before the button
 * leaves the screen. */
@container pane (max-width: 560px) {
  #admin-m365-posts .admin-m365-perms { grid-template-columns: minmax(0, max-content) max-content; }
  #admin-m365-posts .admin-m365-perms .admin-m365-use { contain: inline-size; }
}

/* ── The held meeting beneath a reply draft (IW-868) ─────────────────────────── */
/* A reply draft that proposes a session gets this small card under it: the
 * session as a hold the support person puts on their own calendar. It reads as a
 * calendar entry, not as another draft. The one memorable part is the date leaf,
 * a small tear off calendar page whose top edge is hatched the way Outlook draws
 * a tentative hold, so the card says "held, not yet agreed" before a word is
 * read. The hatch takes the waiting status colour, which is what the session is.
 * Beside the leaf, the time in the support person's own clock leads in the text
 * tone, its clock range kept on one line, the customer's stated time sits under
 * it in the secondary tone, and the length is muted. One accent button opens Outlook; the calendar file beside it
 * is a quiet text link, since it is the second door. */
.meeting-card { max-width: var(--measure-prose); }
.meeting-card .meeting-title { margin-bottom: var(--space-md); }
.meeting-when {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
}
.meeting-leaf {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 52px;
  padding: 10px 0 var(--space-xs);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background:
    repeating-linear-gradient(135deg, var(--color-status-waiting) 0 2px, transparent 2px 5px)
      top / 100% 6px no-repeat,
    var(--color-bg-surface);
  line-height: 1.15;
}
.meeting-leaf-day,
.meeting-leaf-month {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}
.meeting-leaf-date {
  font-size: 22px;
  font-weight: 600;
  color: var(--color-text-primary);
}
.meeting-times { min-width: 0; }
.meeting-times p { margin: 0; }
.meeting-yours {
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text-primary);
}
.meeting-clock { white-space: nowrap; }
.meeting-yours-note { font-weight: 400; color: var(--color-text-muted); }
.meeting-theirs {
  margin-top: 2px;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}
.meeting-length {
  margin-top: 2px;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.meeting-agenda-head {
  margin-top: var(--space-md);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.meeting-agenda {
  margin: var(--space-xs) 0 0;
  padding-left: 1.2em;
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
}
.meeting-agenda li + li { margin-top: 2px; }
.meeting-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-md);
}
.meeting-actions a.meeting-outlook { display: inline-block; text-decoration: none; }
.meeting-file {
  font-size: var(--font-size-sm);
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.meeting-file:hover { color: var(--color-accent-hover); }
.meeting-note {
  margin: var(--space-sm) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* ── end of the held meeting beneath a reply draft (IW-868) ── */

/* IW-986, the way back from a deactivated user. An inactive row now carries
 * Reactivate and Unlink Atlassian, and a dimmed action reads as a disabled one, so
 * the row's facts stay dimmed while its actions keep full strength. This takes over
 * from the whole row dim in the admin console's table rules above. */
.admin-table tr.admin-row-inactive { opacity: 1; }
.admin-table tr.admin-row-inactive > td:not(.admin-actions) { opacity: 0.55; }
