/* ICR2 app shell — Cloudflare-calm: near-monochrome grays, one accent blue,
   generous whitespace, system font stack, no gradients/purple/glassmorphism.
   Per: vault/wiki/sources/Reference Cloudflare Home.md + SCR Sidebar / SCR
   Home Command Bar / SCR Incident Detail / SCR CR Detail.

   Dual light/dark theme (design-polish rulings, 2026-07-20): attribute-
   driven with a system fallback. `:root` below = LIGHT tokens (the
   default). `[data-theme="dark"]` overrides to the DARK tokens (identical
   values to the pre-stage-7 @media block — no visual change for existing
   dark users). The @media (prefers-color-scheme: dark) block re-applies
   the same dark overrides ONLY when no data-theme attribute is present
   (i.e. the user picked "System") — layout.ts/login.ts/invite-accept.ts
   set data-theme server-side from the icr2_theme cookie so there is never
   a flash. Keep the two dark blocks below in sync if either changes. */

:root {
  --gray-950: #14181f;
  --gray-900: #1b212b;
  --gray-800: #262d39;
  --gray-700: #3a4354;
  --gray-500: #6b7686;
  --gray-300: #b9c1cd;
  --gray-200: #dde2e8;
  --gray-100: #eef1f5;
  --gray-50: #f7f8fa;
  --white: #ffffff;
  --accent: #2f6fed;
  /* BACKGROUND-ONLY (ruled 2026-08-22). --accent-dark has never had a dark
     binding, and as TEXT that measured 2.17:1 in dark mode — three chip
     sites and a link hover carried it before being moved to the properly
     dual-bound text roles (--accent-text on bg/surface, --accent-on-tint on
     accent-tint, whose LIGHT binding is this token, so light rendering was
     unchanged by that move). Its one legitimate remaining job is the
     .btn-primary hover fill, where a darker blue under white text passes in
     both themes. Never use it as a text color. */
  --accent-dark: #1f56c9;
  --accent-tint: #eaf1ff;
  /* Theme-invariant like --accent: white text/glyphs sitting directly ON an
     accent FILL, not a tint. Not re-bound in dark — the accent hue itself
     doesn't shift between themes, so its on-color doesn't either. Distinct
     from --accent-text/--accent-on-tint, which are TEXT COLORS FOR accent
     used on --bg/--surface/--accent-tint and DO need dual binding. */
  --on-accent: #ffffff;
  /* Modal/scrim backdrop — <dialog>::backdrop and full-screen scrims only.
     :root-only, theme-invariant: a dark scrim reads correctly over either
     theme's surface. Do NOT fold in the visually-similar-but-different
     overlay values still in use elsewhere (0.38/0.35-alpha, rgb(16 24 40));
     each is a per-surface judgment not reviewed in the 2026-08-22 pass. */
  --backdrop: rgba(20, 24, 31, 0.45);
  /* B3 (ui-design-review-2026-07-20): #b6790a on --amber-tint (#fbf1de)
     measured 3.27:1 — fails WCAG AA for the 11px/600 chip text (below the
     14px-bold/18px-normal "large text" floor, so 4.5:1 is required, not
     3:1). #946008 measures 4.76:1 on the same tint (comfortable margin over
     the reviewer's ~#9c6208 suggestion, which measured exactly 4.50:1) —
     verified programmatically in app.css.contrast.spec.ts. Dark mode's
     #e0a940 already passes (6.63:1) and is untouched. */
  --amber: #946008;
  --amber-tint: #fbf1de;
  --amber-border: #e4c98a;
  /* DEC-028 in-proxy indicator: dedicated semantic names for the proxy
     strip/chip/dashed-ring, deliberately aliased to the SAME hex pair as
     --amber/--amber-tint above (already measured 4.76:1, B3) rather than
     introducing new, unaudited hex values — the PRN-05 acceptance gate this
     DEC calls for is satisfied by reuse, not by a second contrast pass on
     colors that are numerically identical to an already-passing pair. */
  --proxy-banner-bg: var(--amber-tint);
  --proxy-banner-text: var(--amber);
  --proxy-banner-accent: var(--amber);
  --red: #c0392b;
  --red-tint: #f8e5e2;
  --green: #1e7d3e;
  --green-tint: #e2f4e8;

  /* B2 (ui-design-review-2026-07-20): a themed hover-fill token — the raw
     --gray-50 literal it replaces had no dark binding, so any hover using it
     directly painted a flatly light box over dark surroundings (B1). */
  --surface-hover: var(--gray-50);

  /* UX review 2026-07-25 (PRN-05 dual-theme): --accent / --accent-dark have NO
     dark binding, so using them as TEXT failed AA on dark surfaces (avatar
     initials measured 2.17:1; sort-header links 3.76:1). These two role tokens
     carry the accent as *text*; --accent itself stays put because it is also a
     FILL under white text (.btn-primary), where lightening it would break the
     other direction. Both are rebound in BOTH dark bindings below. */
  --accent-text: var(--accent);      /* accent text on --bg / --surface */
  --accent-on-tint: var(--accent-dark); /* accent text on --accent-tint */

  --bg: var(--gray-50);
  --surface: var(--white);
  --border: var(--gray-200);
  --text: var(--gray-900);
  /* B5: nudged one ramp step darker than --gray-500 so --text-quiet on bare
     --bg clears 4.5:1 (measured 4.33:1 before the nudge); --sidebar-text-quiet
     below stays independently overridden per B4, not touched by this. */
  --text-quiet: #66707f;
  --text-soft: var(--gray-700);
  /* Quiet text sitting ON a tinted wash rather than bare --bg (the ticket
     header card). The 4% wash eats --text-quiet's AA margin in light: #66707f
     was tuned against --bg and measures ~4.3-4.4:1 on the wash, and a weaker
     3% mix still lands ~4.45:1, so thinning the fill cannot fix it. One step
     darker, same move as --sidebar-text-quiet and --chat-bubble-meta. Dark
     needs no nudge (#8b96a8 stays >5:1 on the dark wash) but is bound anyway
     so the token is safe to use from any rule without a dark twin. */
  --text-quiet-on-wash: #5d6675;

  /* Chat bubbles (2026-07-28, chat-history audit iter 1). Previously
     --surface-hover, which in the light binding IS --gray-50 — byte-identical
     to --bg, so every other-person message rendered at 1.00:1 against the
     rail: a padded block of text with no visible bubble at all. Dark was
     1.15:1, barely better. These are dedicated tokens rather than a rebind
     because --surface-hover is pinned verbatim by ui-hardening.spec.ts and
     has ~50 unrelated consumers. NOT --surface either: the same bubbles
     render inside .action-dialog (already --surface), which would just move
     the invisibility from the rail into the dialog. Paired with a 1px border
     so the shape survives on BOTH backgrounds regardless of fill contrast.
     Measured (WCAG): fill 1.29:1 vs --bg and 1.41:1 vs --surface.
     --chat-bubble-meta exists because darkening the
     fill pushes bare --text-quiet (#66707f) down to 4.39:1 on it — below AA
     for the 11px timestamp. #555e6b measures 4.80:1 on the other-bubble and
     5.79:1 on the own-bubble (--accent-tint), which also closes a PRE-EXISTING
     failure: --text-quiet on --accent-tint was already 4.42:1 before this
     change. */
  --chat-bubble-other-bg: #d6dde8;
  --chat-bubble-other-border: #c3ccdb;
  --chat-bubble-meta: #555e6b;

  /* Fix 10 (ux-cycle1): a genuinely dark-safe neutral-chip pair — person
     chips / role pills / the "waiting on" chip previously reused bare
     --gray-100 with no paired --color, which is fine in light (dark text
     already the default) but rendered near-white-on-white in dark once
     --text flipped to a light color with the background never re-bound. */
  --chip-bg: var(--gray-100);
  --chip-text: var(--gray-700);

  /* LIGHT sidebar (owner ruling: "pls make dark and light theme" — the
     light binding adopts the light-sidebar direction from the Linear
     preferences reference; dark binding below keeps the shipped
     gray-950 sidebar exactly as-is, per the same ruling). Picked from the
     existing cool-gray ramp (--gray-100), not a new literal. */
  --sidebar-bg: var(--gray-100);
  --sidebar-text: var(--gray-900);
  /* B4 (ui-design-review-2026-07-20): --gray-500 on --sidebar-bg (#eef1f5)
     measured 4.06:1 — fails AA for the 12px .sidebar-empty / zone-b marker
     usages (below the large-text floor). A dedicated override (NOT a change
     to the shared --gray-500 ramp value, which other components still rely
     on) tuned to 5.65:1 against this theme's own --sidebar-bg. */
  --sidebar-text-quiet: #54606f;
  --sidebar-divider: var(--gray-200);
  --sidebar-hover-bg: var(--gray-200);

  /* Linear-preferences grammar tokens (panel-ratified, 2026-07-20). */
  --shadow-card: 0 0 0 0.5px rgba(20, 24, 31, 0.06), 0 3px 6px -2px rgba(20, 24, 31, 0.03), 0 1px 1px rgba(20, 24, 31, 0.04);

  /* MS-15 visual refresh (sponsor 2026-08-02, docs/VISUAL-REFRESH-PLAN.md):
   * a spacing/radius scale — the app had none. Every padding/margin/gap and
   * border-radius up to now was a hand-picked literal px value with no
   * pattern (surveyed: --space-2 through --space-5 alone account for over
   * 200 declarations across the stylesheet). Additive only in this pass —
   * these tokens exist and are correct, but no existing rule has been
   * re-pointed at them yet; that's the deliberately separate "shared chrome"
   * step the plan calls for, reviewed one recipe at a time rather than a
   * blind find-replace (this file's own .command-bar/.field-row history is
   * exactly why: a class shared across two unrelated components without
   * anyone re-checking every reuse site is how two real regressions shipped).
   * Values are the modal points from that survey, not invented: 2/4/6/8/12
   * were already the five most common gap/padding/margin values in the file;
   * this just names them so new and re-mapped CSS can reach for a token
   * instead of a fifth slightly-different literal. */
  --space-1: 2px;
  --space-2: 4px;
  --space-3: 6px;
  --space-4: 8px;
  --space-5: 12px;
  --space-6: 16px;
  /* Radius: 6/8/10px were already the three most common border-radius
   * values (35/29/13 occurrences); 999px ("pill") was already the standard
   * fully-round idiom (29 occurrences) and is named here rather than
   * re-literalized. */
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 10px;
  --radius-pill: 999px;
  /* Type scale (MS-15 Round 45): surveyed every font-size literal in this
   * file (VISUAL-REFRESH-PLAN.md's own item 5 — "10/11/12/13/15/16/24/26px
   * used interchangeably for similar-weight content"). Six steps, picked by
   * real frequency + a coherent semantic role, not evenly spaced math:
   * 11/12/13px were already the three most common sizes in the file
   * (48/89/55 occurrences) and read as quiet-meta / base-UI / secondary-body
   * respectively; 15px is the modal size across every card's h2/h3 and the
   * sticky ticket title (a real "subheading" cluster, 9 occurrences); 24px
   * is Login's and every page-header's h1; 28px is the ticket detail's own
   * title, the single most prominent heading in the product. Landed
   * additive-only, same as the spacing/radius scale in Round 29 — nothing
   * re-pointed at these yet. 14/16/18/26px sit close to a step but read a
   * different UI role at each site (e.g. 26px's home-hero h1 vs 24px's
   * page-header h1 are visually near-identical but were never confirmed
   * as the same intent) — left literal rather than folded in blind, same
   * "don't force it" rule the spacing scale's remap rounds followed. */
  --fs-1: 11px;
  --fs-2: 12px;
  --fs-3: 13px;
  --fs-4: 15px;
  --fs-5: 24px;
  --fs-6: 28px;
}

[data-theme='dark'] {
  --bg: #10141b;
  --surface: #171c25;
  --border: #2a3242;
  --text: #e7ebf1;
  --text-quiet: #8b96a8;
  --text-quiet-on-wash: #8b96a8;
  --text-soft: #b8c1d0;
  --accent-tint: #1b2b4a;
  /* Dark binding of the chat-bubble pair — see the light-theme comment.
     Lifted well clear of --bg (#10141b) and --surface (#171c25) so the
     bubble reads as a bubble on the rail AND inside the expand dialog. */
  --chat-bubble-other-bg: #283243;
  --chat-bubble-other-border: #3a465b;
  --chat-bubble-meta: #939eaf;
  /* Accent-as-TEXT on dark (UX review 2026-07-25): --accent (#2f6fed) is only
     3.76:1 on --surface and 3.10:1 on --accent-tint. These lightened variants
     measure ~6.4:1 and ~5.8:1 respectively. */
  --accent-text: #6f9bff;
  --accent-on-tint: #7aa4ff;
  --amber: #e0a940;
  --amber-tint: #362a10;
  --amber-border: #5a4419;
  --proxy-banner-bg: var(--amber-tint);
  --proxy-banner-text: var(--amber);
  --proxy-banner-accent: var(--amber);
  --red: #e0685a;
  --red-tint: #3a211f;
  --green: #4ade80;
  --green-tint: #17301f;

  /* Fix 10 (ux-cycle1) */
  --chip-bg: #232b3a;
  --chip-text: #c7d0dd;

  /* B2: dark binding of --surface-hover — see the light-theme comment. */
  --surface-hover: #1c222d;

  --sidebar-bg: var(--gray-950);
  --sidebar-text: var(--gray-200);
  /* B4: dedicated override (see the light-theme comment above) — 5.95:1
     against this theme's --gray-950 sidebar-bg (--gray-500 itself measured
     3.86:1, failing AA). --gray-500 elsewhere is untouched. */
  --sidebar-text-quiet: #8b96a8;
  --sidebar-divider: rgba(255, 255, 255, 0.08);
  --sidebar-hover-bg: rgba(255, 255, 255, 0.06);

  /* Dark binding of --shadow-card: a border-led ring (no shadow) reads
     better than a soft drop-shadow on a dark surface (panel ruling). */
  --shadow-card: 0 0 0 1px var(--border);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: #10141b;
    --surface: #171c25;
    --border: #2a3242;
    --text: #e7ebf1;
    --text-quiet: #8b96a8;
    --text-quiet-on-wash: #8b96a8;
    --text-soft: #b8c1d0;
    --accent-tint: #1b2b4a;
    /* Keep in sync with the [data-theme='dark'] block above. */
    --chat-bubble-other-bg: #283243;
    --chat-bubble-other-border: #3a465b;
    --chat-bubble-meta: #939eaf;
    --accent-text: #6f9bff;
    --accent-on-tint: #7aa4ff;
    --amber: #e0a940;
    --amber-tint: #362a10;
    --amber-border: #5a4419;
    --proxy-banner-bg: var(--amber-tint);
    --proxy-banner-text: var(--amber);
    --proxy-banner-accent: var(--amber);
    --red: #e0685a;
    --red-tint: #3a211f;
    --green: #4ade80;
    --green-tint: #17301f;

    /* Fix 10 (ux-cycle1) */
    --chip-bg: #232b3a;
    --chip-text: #c7d0dd;

    /* B2 — keep in sync with [data-theme='dark'] above. */
    --surface-hover: #1c222d;

    --sidebar-bg: var(--gray-950);
    --sidebar-text: var(--gray-200);
    /* B4 — keep in sync with [data-theme='dark'] above. */
    --sidebar-text-quiet: #8b96a8;
    --sidebar-divider: rgba(255, 255, 255, 0.08);
    --sidebar-hover-bg: rgba(255, 255, 255, 0.06);

    --shadow-card: 0 0 0 1px var(--border);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---- App shell ---- */
/* Owner feedback (2026-07-22 — "leftside bar and header can stick only main
   area is scrollable"): height (not min-height) + overflow:hidden turns
   .app-shell into a fixed, viewport-sized box. Its two flex children
   (.sidebar, .app-main) stretch to fill it exactly (default align-items:
   stretch) and each own their own scroll below — the page/body itself never
   scrolls. Scoped to .app-shell only (not body) so the unrelated
   login/invite pages, which never mount this shell, are untouched. */
/* 100dvh, not 100vh (round-table 2026-07-26, decision D6). On a phone `100vh` is
   the LARGE viewport — the height the page would have if the browser's toolbars
   were retracted — so while the toolbar is showing, the bottom of this box sits
   below the visible area. Combined with `overflow: hidden` here, that slice is
   not merely off-screen but unreachable: nothing inside can scroll it into view.
   The mobile list pager is the visible casualty — it sticks to `bottom: 0` of the
   scroll region (see .list-footer[data-truncated] below) specifically so paging
   doesn't require scrolling past every card, and `100vh` buried the thing that
   fix existed to surface. `dvh` tracks the actual visible height instead.
   Precedent for dvh in this file: the command palette's height/max-height. */
.app-shell { display: flex; height: 100dvh; overflow: hidden; }

.sidebar {
  width: 250px;
  flex: none;
  background: var(--sidebar-bg);
  color: var(--sidebar-text);
  padding: 20px 12px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  /* Fix (2026-07-22): overflow-y:auto directly on .sidebar clipped the
     account-menu-panel popup — per spec, giving one axis a non-visible
     overflow value forces the OTHER axis to compute as 'auto' too, so
     content escaping the box horizontally (.sidebar-rail .account-menu-
     panel's left: calc(100% + 8px) flyout) got clipped along with it.
     Scroll now lives on .sidebar-scroll (below), an inner wrapper around
     just the nav links — .sidebar itself stays overflow:visible so the
     profile popup and any future flyout can still escape its bounds. */
}
/* The one thing inside .sidebar that can genuinely grow long (nav links +
   the workspace accordion list) scrolls on its own; the profile row above
   it and the utility footer below stay pinned in view, and .sidebar itself
   never clips anything (see the comment above). */
.sidebar-scroll { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; }

/* D1 (ui-design-review-2026-07-20): mobile sidebar — at 390px the fixed
   250px sidebar consumed 64% of viewport width with no collapse anywhere
   (zero @media rules besides prefers-color-scheme). Both the app sidebar
   AND the admin sidebar use this same .sidebar class, so this one block
   covers both shells. Overlay + scrim reuse the .drawer-overlay z-index
   family already used for the admin/help drawers, keeping one pattern. */
/* sidebar-affordances round table (2026-07-21, owner directive): the
   hamburger is mobile-overlay-only again (Wave 5's "visible at ALL widths"
   desktop-rail-flip behavior is superseded — the sidebar's own
   collapse/expand buttons own desktop rail now, see
   .sidebar-header-actions below). Hidden by default; the mobile media
   query re-enables it. */
.sidebar-toggle {
  display: none;
  align-items: center;
  justify-content: center;
}
@media (max-width: 768px) {
  .sidebar-toggle { display: inline-flex; }
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 95;
    transform: translateX(-100%);
    transition: transform 220ms ease;
    box-shadow: 12px 0 32px rgba(0, 0, 0, 0.18);
  }
  .sidebar.open { transform: translateX(0); }
  /* Rail is a desktop-only state (owner ruling / Consensus): the ≤768px
     overlay ALWAYS shows the full sidebar regardless of the icr2_sidebar
     cookie — mechanically unreachable, not just visually overridden. */
  .sidebar.sidebar-rail { width: 250px; }
  .sidebar-scrim {
    position: fixed;
    inset: 0;
    background: rgba(20, 24, 31, 0.35);
    z-index: 94;
  }
  .sidebar-scrim[hidden] { display: none; }
  .content { padding: 20px 16px; max-width: 100%; }
  /* The desktop icon cluster (search/collapse/expand) is inert on this
     breakpoint — rail is mechanically unreachable here (above) — so a
     visible-but-dead control would be a dishonest affordance. The labeled
     Search row (.sidebar-search-btn, renderPaletteButton) is this
     breakpoint's only search entry point. Compound selector (not the bare
     class) so this reliably beats the unconditional .sidebar-header-actions
     { display: flex } rule below regardless of source order — same
     specificity, single-class rules are fragile to reordering. */
  .sidebar .sidebar-header-actions { display: none; }
}
@media (min-width: 769px) {
  /* Desktop keeps the icon-only cluster; the labeled row is mobile-only.
     Compound selector: the bare class loses to the later .sidebar-link
     { display: flex } rule (same specificity, later source order) — the
     exact bug that shipped a duplicate desktop search row (DEC-025 fix,
     owner-reported 2026-07-21). */
  .sidebar .sidebar-search-btn { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
}

/* ---- Sidebar icon rail (Wave 5, linear-refinement — DEC-003 Amendment)
   ~56px collapse, Zone A + utility footer only. Gated behind
   min-width:769px (desktop-only by construction) so this whole block is
   mechanically inert on the shipped ≤768px overlay layer above. Applies
   identically to BOTH shells (owner ruling: rail parity everywhere in v1,
   overruling the panel's admin exclusion) since both already share every
   .sidebar rule (DEC-011 A1). Zone B (workspace accordions, app AND admin)
   is hidden entirely — DEC-003's un-iconable-identical-children objection
   holds equally for admin's per-workspace tree; ⌘K / the palette button is
   the recorded jump path for both. Token-only; no new colors. */
@media (min-width: 769px) {
  .sidebar { transition: width 200ms ease; }
  .sidebar-rail {
    width: 56px;
    padding: 20px 8px;
  }
  /* sidebar-affordances round table (2026-07-21): the profile row stacks
     vertically in rail (side-by-side is arithmetically impossible — 28px
     avatar + 32px button > the ~40px rail inner width). DOM order (avatar,
     name, header-actions-cluster) plus this column flip yields the visual
     stack avatar → expand → search with zero JS attribute mutation — name
     is hidden below regardless of its DOM position. */
  .sidebar-rail .sidebar-profile { flex-direction: column; align-items: center; gap: 8px; padding: 0 0 12px; }
  .sidebar-rail .user-name { display: none; }
  .sidebar-rail .sidebar-link-label > span:last-child { display: none; }
  .sidebar-rail .sidebar-link {
    justify-content: center;
    padding: 8px;
    position: relative;
  }
  .sidebar-rail .sidebar-zone-b { display: none; }
  .sidebar-rail .sidebar-empty { display: none; }
  .sidebar-rail .sidebar-admin-hint { display: none; }
  .sidebar-rail .sidebar-admin-wordmark { justify-content: center; padding: 4px 0 12px; }
  /* Admin's wordmark row is the structural equivalent of the app's profile
     row (admin has no avatar) — same column-stack treatment so its header
     actions cluster reads back-arrow → shield → expand → search. */
  .sidebar-rail .sidebar-admin-header { flex-direction: column; gap: 8px; }
  .sidebar-rail .sidebar-header-actions { flex-direction: column; gap: 8px; }
  /* Two static state buttons per control (owner ruling/Consensus): CSS
     swaps which one is visible/tabbable, never a mutated aria-label.
     Compound selectors (not the bare .sidebar-expand-btn class) so this
     reliably beats .sidebar-icon-btn's own `display: flex` regardless of
     source order — same specificity, single-class rules are fragile to
     reordering; a compound selector is not. */
  .sidebar-header-actions .sidebar-expand-btn { display: none; }
  .sidebar-rail .sidebar-header-actions .sidebar-expand-btn { display: flex; }
  .sidebar-rail .sidebar-header-actions .sidebar-collapse-btn { display: none; }
  /* The avatar popup anchors beside the 56px column in rail (below it
     would be clipped/awkward against the icon-only rail). */
  .sidebar-rail .account-menu-panel { top: 0; left: calc(100% + 8px); }
  /* Badges collapse to a small count dot on the icon (rail: 'where's the
     fire without a click' still holds — Maya's SCR Sidebar charter) rather
     than disappearing outright. */
  .sidebar-rail .badge {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 8px;
    height: 8px;
    padding: 0;
    font-size: 0;
    line-height: 0;
  }
}

.sidebar-profile { display: flex; align-items: center; gap: 10px; padding: 0 8px 12px; border-bottom: 1px solid var(--sidebar-divider); }
.user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--accent); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; }
/* sidebar-affordances round table (2026-07-21): the row now competes for
   width with the search/collapse icon cluster — min-width:0 + flex:1 +
   ellipsis is mandatory the moment a long name meets fixed-width buttons
   (verified with a ~40-char name; demo personas are all short enough to
   hide the bug). */
.user-name { font-weight: 600; font-size: 13px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }

/* ---- Avatar account-menu popup (owner ruling, sidebar-affordances
   2026-07-21) — native <details>/<summary>, the same disclosure mechanism
   as every other "…" card menu in this codebase (e.g. .card-menu). No new
   document-level click-outside/Esc listener; closes the way every other
   card menu does, by clicking its own trigger again. */
.account-menu { position: relative; flex: none; }
.sidebar-avatar-btn { list-style: none; cursor: pointer; display: flex; border-radius: 50%; }
.sidebar-avatar-btn::-webkit-details-marker { display: none; }
.sidebar-avatar-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.account-menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 20;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  min-width: 220px;
  padding: 8px;
}
.account-menu-header { padding: 6px 8px 10px; border-bottom: 1px solid var(--border); margin-bottom: 6px; }
.account-menu-name { font-weight: 600; font-size: 13px; color: var(--text); }
.account-menu-email { font-size: 12px; color: var(--text-quiet); margin-top: 2px; }
.account-menu-logout { margin: 0; }
.account-menu-logout .card-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; }

/* ---- Sidebar search/collapse/expand icon cluster (sidebar-affordances
   round table, 2026-07-21) — replaces the desktop hamburger and the
   full-width Search row on desktop. 32x32 clears WCAG 2.5.8 (24px min)
   with margin; 16px glyphs (icon() default) at --sidebar-text-quiet
   resting, raising to --sidebar-text on hover — both already dual-theme
   AA-audited tokens (B4 fix), so this is a verification pass, not new
   color work. */
.sidebar-header-actions { display: flex; align-items: center; gap: 4px; }
.sidebar-icon-btn {
  width: 32px;
  height: 32px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  border-radius: 6px;
  color: var(--sidebar-text-quiet);
  cursor: pointer;
}
.sidebar-icon-btn:hover { background: var(--sidebar-hover-bg); color: var(--sidebar-text); }
.sidebar-icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Admin's wordmark row is the structural equivalent of the app's profile
   row (admin has no avatar) — same header-actions cluster mounts at its
   right edge. */
.sidebar-admin-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

.sidebar-zone-a, .sidebar-zone-b { display: flex; flex-direction: column; gap: 2px; }

.sidebar-link {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 10px;
  border-radius: 6px;
  color: var(--sidebar-text);
  font-size: 13px;
}
.sidebar-link:hover { background: var(--sidebar-hover-bg); text-decoration: none; }
.sidebar-link.active { background: var(--accent); color: white; }
.sidebar-link-label { display: inline-flex; align-items: center; gap: 8px; }

.icon { flex: none; vertical-align: -3px; }

/* Wave 1 (linear-refinement, 2026-07-20): palette 'Search' row — reuses the
   .sidebar-link shape/hover rules on a <button> (border/background/width/
   text-align/font/cursor reset so it renders identically to the <a> rows
   above and below it). sidebar-affordances round table (2026-07-21): this
   labeled row is mobile-only now (hidden ≥769px above); its ⌘K kbd-hint
   chip relocated into the quick-search overlay (below) — the class keeps
   a live consumer there, so it isn't dead CSS. */
/* Wave 2: the relocated help trigger (button, was an <a>-adjacent .btn in
   the topbar) reuses the same reset as the search row above it. */
/* `font: inherit` here until 2026-08-14, which is the button de-styling idiom
   and also a SHORTHAND: it reset font-size too, so these two buttons rendered
   at the inherited 14px while the ten real links beside them sat at
   `.sidebar-link`'s 13px. Two of twelve sidebar items were visibly bigger than
   the rest, on every page. Found by ui-lint's unreachable-rule sweep and
   measured in a browser. The family is what a button actually needs
   overriding; the SIZE belongs to `.sidebar-link`, which is later-wins only
   because this rule stopped claiming it. */
.sidebar-search-btn, .sidebar-help-btn {
  width: 100%;
  border: none;
  background: transparent;
  font-family: inherit;
  font-weight: inherit;
  line-height: inherit;
  cursor: pointer;
  text-align: left;
}
.kbd-hint {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px;
  font-weight: 600;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--chip-bg);
  color: var(--chip-text);
}

.sidebar-empty { color: var(--sidebar-text-quiet); font-size: 12px; padding: 8px 10px; }

.sidebar-zone-b-item { border-radius: 6px; }
.sidebar-zone-b-item summary { cursor: pointer; padding: 8px 10px; font-size: 13px; font-weight: 600; list-style: none; }
.sidebar-zone-b-item summary::-webkit-details-marker { display: none; }
.sidebar-zone-b-item summary::before { content: '▸ '; color: var(--sidebar-text-quiet); }
.sidebar-zone-b-item[open] summary::before { content: '▾ '; }
.sidebar-zone-b-children { display: flex; flex-direction: column; padding-left: 10px; }

/* Round 74 (MS-27 item 4, "wait, which client am I in" disorientation) —
   a small decorative dot, deterministic per workspace (components.ts's
   workspaceDotColor, a stable hash of the slug — no DB column, no admin
   picker). Locked narrow scope: sidebar workspace tree + ticket detail
   header ONLY. Do not add this class to list rows, cards, or admin chrome
   without a new authorizing ticket — the roundtable panel's written
   hard-stop against silent expansion (.roundtable/icr2-roadmap-finetune.md). */
.ws-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }

.badge {
  background: var(--accent);
  color: white;
  font-size: 11px;
  font-weight: 600;
  padding: 1px 7px;
  border-radius: 999px;
}
.sidebar-link.active .badge { background: rgba(255,255,255,0.25); }

/* min-height: 0 is the standard flex-column fix: without it, a flex item
   (.content, below) can't shrink below its content's natural height, which
   would silently defeat overflow-y:auto and just grow .app-main (and the
   whole page) taller instead of scrolling internally. */
.app-main { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 28px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex: none;
}
.topbar-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.brand { font-weight: 700; font-size: 16px; color: var(--text); flex: none; }
/* sidebar-affordances round table (2026-07-21): the user chip, theme
   toggle, and logout button relocated into the sidebar avatar popup
   (.account-menu-panel). Owner feedback (2026-07-22): the demo-mode chips
   followed them into the popup too (see .account-menu-demo below), so
   .topbar-right (their old home) is deleted with its markup — dead CSS
   invites reuse. The topbar now holds only the mobile hamburger, brand,
   and the breadcrumb (.topbar-left .breadcrumb, above).

   2026-07-28 AMENDMENT (owner ask: "Top-right of header add workspace
   filter"): .topbar-right is REINSTATED, for the workspace filter only.
   This does not reopen the ruling above — that moved the ACCOUNT surface
   (user chip / theme / logout / demo chips) into the sidebar popup, and
   those all stay there. A workspace filter is page scope, not account
   identity, so the header is where it belongs. If a future change wants to
   put account controls back here, that IS the settled ruling and needs the
   round table again. */
.topbar-right { display: flex; align-items: center; gap: 8px; flex: none; }

/* Workspace filter — a .list-pop popover (shares the list toolbar's
   summary/panel styling) anchored to the header's right edge. */
.ws-filter .list-pop-panel { min-width: 240px; max-width: 320px; }
.ws-filter-hint { margin: 0 0 6px; }
.ws-filter-list { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }
.ws-filter-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text);
}
.ws-filter-item:hover { background: var(--surface-hover, var(--bg)); }
.ws-filter-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-filter-actions {
  display: flex;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
/* On narrow screens the label collapses to the icon (same pattern the list
   toolbar buttons use) so the breadcrumb keeps its room. */
@media (max-width: 720px) {
  .ws-filter .btn-label { display: none; }
  .ws-filter .btn-icon-only { display: inline-flex; }
}

.demo-switcher { display: flex; gap: 6px; flex-wrap: wrap; }
.demo-switcher-form { display: inline; }
.account-menu-demo { padding: 6px 12px 2px; border-top: 1px solid var(--border); margin-top: 4px; }
.account-menu-demo-label { font-size: 11px; text-transform: uppercase; color: var(--text-quiet); margin: 0 0 6px; }

/* ---- Theme toggle (design-polish rulings bullet 3, 2026-07-20): three
   states — Light / Dark / System — rendered in the header user-menu area
   on every shell page, and pinned top-right on the unauthenticated
   login/invite pages (.login-page .theme-toggle below). ---- */
.theme-toggle { display: inline-flex; gap: var(--space-1); padding: var(--space-1); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); }
.theme-toggle-btn { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: transparent; border-radius: 6px; color: var(--text-quiet); cursor: pointer; }
.theme-toggle-btn:hover { background: var(--gray-100); }
.theme-toggle-btn.active { background: var(--accent-tint); color: var(--accent-on-tint); }
.login-page .theme-toggle { position: absolute; top: 16px; right: 16px; }

/* 1100px → 1680px (sponsor: fill a 1920px monitor specifically). Sidebar
   is a fixed 250px (.sidebar above), so a 1920px browser window leaves
   ~1670px for .content — 1680px fills that with ~zero dead margin at the
   most common desktop resolution, while still being a real ceiling (not
   fully fluid) on ultrawide monitors beyond that. General dashboard
   guidance centers lower (~1140-1280px) purely for reading-line-length,
   but that concern is already handled separately: long-form prose (ticket
   detail, root cause/solution) keeps its own 68ch cap via
   .md-body--reading regardless of this value — this cap is sized for the
   table/list screens instead. */
/* flex:1 + overflow-y:auto is the actual scroll container now — the
   sidebar and header stay put (see .app-shell above); only this scrolls. */
/* `position: relative` is load-bearing, not decoration (2026-07-30, sponsor:
   "fix the scrolling on Change Request page it has the space below screen when
   scroll up").

   THE SCROLLER MUST BE A POSITIONING CONTEXT. .app-shell is height:100dvh /
   overflow:hidden and THIS is the only scrolling box, so the window should never
   scroll at all. It did: every `.visually-hidden` screen-reader span is
   position:absolute, and with no positioned ancestor inside this box their
   containing block was the INITIAL containing block — the document. An absolutely
   positioned box is only clipped by an ancestor that is in its containing-block
   chain, so those spans escaped this scroller and stretched the DOCUMENT's scroll
   height to wherever the deepest one sat.

   Measured on the CR page before the fix: document.scrollHeight 1359 against a
   892px viewport, matching the deepest .visually-hidden span's bottom to the
   pixel. So the window could scroll ~467px, and scrolling it slid the entire
   fixed shell upward and exposed empty page background underneath — the "space
   below screen". Worse in the other direction: at that scroll position the last
   ~110px of the activity rail sat below the fold and could not be reached, because
   the real scroller had already hit its own end. After: document.scrollHeight 892
   (exactly the viewport), the window cannot scroll, and this box still scrolls its
   1518px of content normally.

   It is deliberately fixed HERE rather than on .visually-hidden: the same escape
   applies to any absolutely positioned descendant, and the sr-only recipe is the
   standard one and is not the thing that is wrong. Side effect, checked and kept:
   .action-dialog (left:0/right:0/margin:auto) now centres on the content pane
   instead of the whole window, which is where the dialogs' own content lives. */
.content { padding: 32px 40px; max-width: 1680px; flex: 1; overflow-y: auto; position: relative; }

/* ---- Buttons / forms ---- */
.btn {
  font: inherit;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-dark); }

/* The primary create action: it is the one thing on the page a user comes to DO,
   so it earns a little more presence than a plain filled rectangle — a leading
   +, slightly tighter tracking, and a soft accent-tinted lift that deepens on
   hover. Kept inside the existing token palette (no new colours). */
.btn-new {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-weight: 600;
  letter-spacing: -0.01em;
  box-shadow: 0 1px 2px rgba(47, 111, 237, 0.28);
  transition: background 120ms ease-out, box-shadow 120ms ease-out, transform 120ms ease-out;
}
.btn-new:hover { box-shadow: 0 3px 8px rgba(47, 111, 237, 0.34); text-decoration: none; }
.btn-new:active { transform: translateY(1px); box-shadow: 0 1px 2px rgba(47, 111, 237, 0.3); }
.btn-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .btn-new { transition: none; }
  .btn-new:active { transform: none; }
}
.btn-quiet { background: transparent; color: var(--text-quiet); }
/* 2026-07-23 follow-up: the attachment-delete confirm dialog's destructive action. */
.btn-danger { background: var(--red); border-color: var(--red); color: white; }
.btn-danger:hover { filter: brightness(0.9); }
/* The REJECT half of the decision grammar (sponsor 2026-08-08: "reject should
   be border with light red?" — ruled then, recorded in DESIGN-SYSTEM.md as
   outstanding, built in the 2026-08-09 consistency sweep). An OUTLINE, not a
   fill: .btn-danger is for destructive acts (delete), while a Reject is a
   legitimate decision that should read as available-but-not-inviting. The
   border mixes toward the surface so it stays light in both themes off the one
   token; text stays --text, not red — a red-lettered button beside a filled
   primary would shout louder than the primary. */
.btn-reject { border-color: color-mix(in srgb, var(--red) 45%, var(--surface)); }
.btn-reject:hover { border-color: var(--red); }
.chip-demo { padding: 4px 10px; font-size: 12px; }

.quiet-link { color: var(--text-quiet); font-size: 13px; }

.form { display: flex; flex-direction: column; gap: var(--space-6); max-width: 560px; }
.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field-row { display: flex; gap: var(--space-6); }
.field-row .field { flex: 1; }
.invite-role-form { display: flex; gap: var(--space-2); align-items: center; }
label { font-size: 12px; font-weight: 600; color: var(--text-quiet); }
input, select, textarea {
  font: inherit;
  padding: 8px 10px;
  border: 1px solid var(--border);
  /* --radius-sm, tokenized with .column-picker-ord/.saved-view-name (2026-08-22)
     so a future --radius-sm change moves ALL input-shaped controls together. */
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}
.form-hint { font-size: 12px; color: var(--text-quiet); margin: 0; }
.form-error { color: var(--red); font-size: 13px; }
.required-marker { color: var(--red); }

/* Fix 10 (ux-cycle1): input[type=file] has no rule of its own anywhere in
   this file — the base `input, select, textarea` background/color rule
   above doesn't reach the native file-picker button, so it stayed
   light-themed always. ::file-selector-button is the cross-browser hook. */
input[type='file'] {
  color: var(--text-quiet);
}
input[type='file']::file-selector-button {
  font: inherit;
  padding: 6px 12px;
  margin-right: 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--chip-bg);
  color: var(--text);
  cursor: pointer;
}

.workspace-picker-form { margin-bottom: 20px; display: flex; gap: 8px; align-items: center; }
/* 2026-08-23 (create-form redesign): the picker now renders HIDDEN under the
   header eyebrow, revealed by the "change" link — and a class that sets
   display overrides the browser's own [hidden] rule, the exact fault
   chat-history-render.spec.ts's structural guard exists to catch (and did,
   in the same change that introduced the hidden usage). */
.workspace-picker-form[hidden] { display: none; }

/* ---- Login ---- */
.login-page { position: relative; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: var(--bg); }
/* MS-15 (Round 36): --shadow-card existed, audited and dark-safe, but was
   only ever applied to overlays (menus, typeahead) — the first-impression
   screen in the whole app had zero elevation, a 1px border its only
   separation from the page, barely visible in light theme where --surface
   (white) sits on --bg (#f7f8fa). Card padding/radius stay literal (40px,
   12px) — a deliberately larger "hero card" tier the component-level
   --space-N/--radius-N scale was never meant to cover; forcing them onto
   --space-6/--radius-lg would shrink the login card, a real visual change
   dressed as a token swap. */
.login-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-card); padding: 40px; width: 360px; }
.login-brand-corner { position: fixed; right: 16px; bottom: 16px; }
.login-brand-logo { height: 48px; width: auto; background: var(--white); padding: var(--space-4) 14px; border-radius: var(--radius-md); }
.login-title { margin: 0; font-size: 24px; }
.login-subtitle { color: var(--text-quiet); margin: var(--space-2) 0 24px; font-size: 13px; }
.demo-panel { margin-top: 28px; border-top: 1px solid var(--border); padding-top: var(--space-6); }
.demo-panel-title { font-size: 12px; color: var(--text-quiet); margin: 0 0 var(--space-4); }
.demo-panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.demo-panel-grid form { margin: 0; }
.demo-panel-grid .btn { width: 100%; }

/* ---- Home / command bar (SCR Home Command Bar, DEC-006) ---- */
.home-hero {
  text-align: center;
  padding: 40px 0 32px;
  transition: padding 240ms ease;
}
.home-hero h1 {
  font-size: 26px;
  font-weight: 600;
  margin-bottom: 20px;
  max-height: 60px;
  opacity: 1;
  overflow: hidden;
  transition: max-height 240ms ease, opacity 180ms ease, margin-bottom 240ms ease;
}
/* Docking morph (owner's signature idea, home-only, <=300ms): the greeting
   collapses and the bar pins toward the top of the content area. */
.home-hero.docked { padding: 12px 0 20px; }
.home-hero.docked h1 { max-height: 0; opacity: 0; margin-bottom: 0; }

/* 640px -> 900px (sponsor: use full width everywhere). Kept well short of
   .content's 1680px on purpose — a single-line search input spanning
   nearly the whole screen reads as broken, not spacious; this is wide
   enough to visibly benefit from the wider layout without that. */
.command-bar-wrap { position: relative; max-width: 900px; margin: 0 auto; }
/* Home Command Centre intake bar — flat, in normal flow. No shadow, no glass,
   no translucency: nothing sits beneath it, so elevation would be a lie. The
   input carries the whole affordance (one box, not a padded card wrapping a
   second bordered box), and prominence comes from size and the focus ring
   rather than from a drop shadow. */
.command-bar { display: flex; gap: var(--space-4); }
.home-hero .command-bar { background: none; border: none; box-shadow: none; padding: 0; border-radius: 0; backdrop-filter: none; -webkit-backdrop-filter: none; }
.home-hero .command-input { border: 1px solid var(--border); background: var(--surface); border-radius: var(--radius-lg); }
.home-hero .command-input:focus-visible,
.home-hero .command-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
/* padding stays 14px/18px literal — neither matches a surveyed value
   (nearest are 12px/16px), left rather than forced. */
.command-input { flex: 1; padding: 14px 18px; font-size: 15px; border-radius: var(--radius-lg); }
.command-bar-hint { margin-top: 14px; }

/* Inline shimmer while classify is in flight — never a blocking spinner. */
.command-bar-shimmer {
  display: none;
  gap: 5px; /* stays literal — doesn't match a surveyed value (nearest 4/6px) */
  justify-content: center;
  padding: var(--space-4) 0 0;
}
.command-bar-wrap.htmx-request .command-bar-shimmer { display: flex; }
.command-bar-shimmer span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.35;
  animation: command-bar-shimmer-pulse 900ms ease-in-out infinite;
}
.command-bar-shimmer span:nth-child(2) { animation-delay: 150ms; }
.command-bar-shimmer span:nth-child(3) { animation-delay: 300ms; }
@keyframes command-bar-shimmer-pulse {
  0%, 100% { opacity: 0.25; transform: scale(0.85); }
  50% { opacity: 0.9; transform: scale(1.15); }
}

/* Fix 6 (ux-cycle1): visible in-flight state on the Proceed button itself
   (the shimmer above sits below the bar and read as pixel-identical to
   idle at 1-2s in the UX simulation) — a small spinner replaces the label
   text while the button is disabled (hx-disabled-elt) during classify. */
.command-bar button[disabled] .btn-spinner {
  display: inline-block;
  width: 13px;
  height: 13px;
  border: 2px solid color-mix(in srgb, var(--on-accent) 40%, transparent);
  border-top-color: var(--on-accent);
  border-radius: 50%;
  animation: btn-spinner-spin 700ms linear infinite;
}
.btn-spinner { display: none; }
.command-bar button[disabled] .btn-label { display: none; }
@keyframes btn-spinner-spin {
  to { transform: rotate(360deg); }
}

.intake-result { max-width: 900px; margin: 0 auto; }
.intake-fragment { margin-top: 6px; }

/* State 3 (Docked/confident): AI-suggested type + one-click switch. */
.ai-switch-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.ai-switch-link { font-size: 12px; }
.ai-sparkle { font-size: 11px; }

/* AI pre-filled fields: tinted background + sparkle glyph in the label;
   clears the instant the user touches the field (see layout.ts script). */
input.ai-prefilled, select.ai-prefilled, textarea.ai-prefilled {
  background: var(--accent-tint);
  border-color: var(--accent);
}

/* States 4/5 (Ambiguous / Failed-offline): the candidate picker. */
.intake-ai-note, .quiet-note { color: var(--text-quiet); font-size: 12px; margin: 0 0 10px; }
.candidate-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.candidate-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  text-align: left;
}
.candidate-card:hover,
.candidate-card.active { border-color: var(--accent); text-decoration: none; background: var(--accent-tint); }
.candidate-key {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: var(--gray-100);
  color: var(--text-quiet);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.candidate-body strong { display: block; font-size: 13px; }
.candidate-sub { font-size: 12px; color: var(--text-quiet); }

/* A1/A2 (ui-design-review-2026-07-20): minmax(0, …) is the standard CSS
   grid-blowout fix — an `fr` track's implicit minimum is `auto` (its
   content's min-content) unless overridden, so nowrap/ellipsis children in
   one column can balloon that track and starve the others. data-cols tracks
   the actual rendered column count (3 for most personas, 4 for Support,
   where unassignedNeedsTriage adds a 4th `.home-column`) so the grid gets a
   true N-up row instead of the 4th item auto-wrapping into a half-empty
   row 2. See SCR Home Command Bar "Layout regions". */
.home-columns { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; margin-top: 24px; }
.home-columns[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1024px) {
  .home-columns[data-cols='4'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  /* Both selectors needed: [data-cols='4'] has higher specificity than the
     bare class, so without this the 1024px rule above would keep winning at
     640px and below (both media queries are simultaneously true at, say,
     390px — CSS specificity, not media-query order, decides the winner). */
  .home-columns,
  .home-columns[data-cols='4'] { grid-template-columns: 1fr; }
}
.home-column h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-quiet); margin-bottom: 10px; }

/* A3: dock choreography — columns transition out beneath the docked
   command bar and back in on any un-dock trigger (Esc / input cleared /
   explicit Cancel — see home.ts's inline script). ~260-280ms per the
   review's prescription; prefers-reduced-motion collapses to a near-instant
   opacity swap. */
.home-columns {
  transition: opacity 260ms ease, transform 260ms ease, max-height 280ms ease, margin-top 280ms ease;
  transform: translateY(0);
  max-height: 2000px;
  overflow: hidden;
}
.home-columns.docked-hidden {
  opacity: 0;
  transform: translateY(-8px);
  max-height: 0;
  margin-top: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .home-columns { transition: opacity 1ms linear; }
}

/* ---- ⌘K quick search (DEC-006 owner ruling: search, not intake) ---- */
.quick-search-overlay {
  position: fixed;
  inset: 0;
  background: var(--backdrop);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 12vh;
  z-index: 100;
}
.quick-search-overlay[hidden] {
  display: none;
}
.quick-search-modal {
  position: relative;
  width: 100%;
  max-width: 560px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
  overflow: hidden;
}
.quick-search-icon { position: absolute; left: 18px; top: 18px; color: var(--text-quiet); pointer-events: none; }
.quick-search-input {
  width: 100%;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 16px 18px 16px 42px;
  font-size: 15px;
}
.quick-search-input:focus { outline: none; }
.quick-search-results { max-height: 320px; overflow-y: auto; }
.quick-search-list { list-style: none; margin: 0; padding: 6px; }
.quick-search-item { display: block; padding: 10px 12px; border-radius: 6px; color: var(--text); font-size: 13px; }
.quick-search-item:hover,
.quick-search-item.active { background: var(--surface-hover); text-decoration: none; }
.quick-search-hint { padding: 10px 12px; }
/* Fix 9 (ux-cycle1): link/pin typeahead — reuses .quick-search-item as a
   <button> (fills the input) instead of an <a> (navigates). */
.typeahead-list { position: relative; z-index: 5; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow-card); }
button.quick-search-item { width: 100%; text-align: left; background: none; border: none; font: inherit; cursor: pointer; display: flex; align-items: center; gap: 8px; }
.field-error { color: var(--red); font-size: 12px; margin: 4px 0 0; }

.list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.list-row:hover { text-decoration: none; background: var(--surface-hover); }

/* B1 (ui-design-review-2026-07-20): admin Members table used .list-row (a
   flex recipe) directly on a <tr>, forcing display:flex on a table row and
   breaking column alignment with <thead> (row collapsed to content width,
   255px vs the table's 1020px). Own class, no display override at all —
   table rows keep their default display:table-row. Themed hover via
   --surface-hover (light --gray-50 / dark #1c222d) instead of the untthemed
   raw literal that flashed a light box in dark mode. */
.admin-member-row { cursor: pointer; }
.admin-member-row:hover { background: var(--surface-hover); }
/* Fix (ux-cycle3), reusing the fix-11 (ux-cycle1) list-page row recipe:
   case-over-subject stacked in ONE flexible cell with min-width:0 (a flex
   child otherwise refuses to shrink below its content size, which is what
   produced the one-word-per-line dashboard title towers) and ellipsis on
   the subject line so long titles truncate instead of wrapping. */
.list-row-case-subject { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 2px; }
/* 2026-07-28: in a narrow dashboard column, a long status label ("Approve
   Proposal", "Solution Proposed") grew its chip until the case-number cell
   collapsed and "CR-ZSD-2026-002" wrapped one fragment per line. The chips are
   secondary metadata, so they shrink and ellipsize; the case number never
   wraps, because it is the row's identity. */
.list-row-case { white-space: nowrap; }
.list-row .chip-status, .list-row .chip-workspace {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.list-row-case { font-weight: 600; font-size: 12px; color: var(--text-quiet); flex: none; }
/* .list-row-subject keeps flex:1 for its older direct-sibling-of-.list-row
   call sites (boardroom.ts, project-detail.ts); the overflow rules are
   inert there without a bounded (min-width:0) box, and become active for
   the home.ts dashboard rows once nested in .list-row-case-subject above. */
.list-row-subject {
  flex: 1;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.empty-state { color: var(--text-quiet); font-size: 13px; padding: 20px; border: 1px dashed var(--border); border-radius: 8px; text-align: center; }
/* MS-15 (Round 37): the icon is decorative (aria-hidden, see components.ts)
   and inherits --text-quiet via currentColor — no separate color rule
   needed. display:block + margin:auto centers it since the SVG itself
   isn't a text-align target. */
.empty-state-icon { display: block; margin: 0 auto var(--space-3); }
.empty-state-heading { color: var(--text); font-weight: 600; margin: 0 0 4px; }
.empty-state-body { color: var(--text-quiet); margin: 0; }

/* ---- Page header / tabs / table ---- */
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; gap: 12px; }
/* Linear type rhythm (panel-ratified 2026-07-20): page titles ~24px/500
   where headers exist; 14px body floor unchanged elsewhere. */
.page-header h1 { font-size: 24px; font-weight: 500; margin: 0; }

.tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--border); }
/* .tab renders as a plain <a> in most callers (ticket-list.ts's Open/All/
   Closed), but two (md-editor-field.ts, admin platform-settings.ts) use a
   real <button> — which keeps native UA chrome (a light background, an
   outset border) unless every property that chrome sets is reset here, not
   just the ones a link would already lack. */
.tab {
  appearance: none;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
  padding: 8px 14px;
  font-size: 13px;
  color: var(--text-quiet);
  border-bottom: 2px solid transparent;
}
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab:hover { text-decoration: none; }

/* ---- Phase 4: quick-view presets (pill row above the list toolbar) ---- */
.quick-views { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.quick-view {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  font-size: 13px;
  color: var(--text-quiet);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.quick-view:hover { text-decoration: none; background: var(--surface-hover); color: var(--text); }
.quick-view.active { color: var(--accent-on-tint); border-color: var(--accent); background: var(--accent-tint); }
.quick-view .n {
  min-width: 18px;
  padding: 0 6px;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  border-radius: 999px;
  background: var(--chip-bg);
  color: var(--chip-text);
}
.quick-view.active .n { background: var(--accent); color: white; }

/* ---- Phase 4: row-count footer (below the list table) ---- */
.list-footer { margin-top: 10px; font-size: 12px; color: var(--text-quiet); }
.list-footer[data-truncated="true"] .list-footer-count { font-weight: 600; color: var(--text); }

/* Screen-reader-only utility — used by the column picker's aria-live announcer
   (Phase 3). Standard clip pattern; visible to AT, invisible on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- List toolbar + column picker (Phase 3) ---- */
.list-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.list-toolbar:empty { display: none; }
/* When no search form renders (ws routes), the action cluster still right-aligns. */
.list-toolbar > .list-toolbar-actions { margin-left: auto; }
/* The toolbar is now Search . Settings . Export -- one popover plus one link. */
.list-toolbar-actions { display: flex; align-items: center; gap: 8px; }
.list-search { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.list-search input[type="search"] {
  min-width: 240px; max-width: 100%; padding: 6px 10px; font-size: 13px;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text);
}

/* Button icons: a small inline stroke glyph before the label. Sized in `em` so
   it stays optically proportional on both the 12px toolbar labels and 14px
   buttons; spacing comes from the control's own `gap` (a margin here would
   double up against gap). */
.btn-icon { width: 1.15em; height: 1.15em; flex: none; }
/* Toolbar controls carry both a labelled and an icon-only variant; the label
   wins on desktop, the icon on small screens (owner request 2026-07-26). The
   control keeps an aria-label either way, so the accessible name never depends
   on which variant is visible. */
.btn-label { display: inline-flex; align-items: center; gap: var(--space-3); }
.btn-icon-only { display: none; }

/* ONE toolbar control height. Previously the Settings <summary> computed 32px
   while <a class="btn"> Export computed 39px, and the row was align-items:
   flex-start — so Export hung 7px lower with a ~3.5px baseline offset (UX
   review 2026-07-25). Every control in the toolbar now shares one box. */
.list-toolbar .btn,
.list-toolbar summary.btn-quiet,
.list-toolbar .list-export-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 12px;
  font-size: 12px;
  line-height: 1;
}
.list-search input[type="search"] { height: 32px; padding: 0 10px; }

.settings-section-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--text-quiet); margin-bottom: 6px;
}
.settings-hint { font-size: 11px; color: var(--text-quiet); margin: 0 0 8px; }
.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 10px; }

.filter-count {
  display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px;
  border-radius: 8px; background: var(--accent); color: white;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
}

/* Quiet inline text fragments (12px --text-quiet, no margin) — five sites,
   one idea: quiet priority/effort figures, admin status words, inheritance
   notes. Grouped 2026-08-22, dead-weight removal Phase 1 (pixel-identical).
   NOT folded into .quiet-note: that recipe carries margin: 0 0 10px for its
   caption/empty-state block role, and these are inline fragments inside
   rows/cells where that margin would change layout. See DESIGN-SYSTEM.md
   "Quiet inline text" entry. (.cond-sep, the sixth original member, was
   retired tick 80 with the ad-hoc condition builder it belonged to — an
   owner-ruled dead feature, .roundtable/customisable-table-v2.md "F3".) */
.priority-quiet,
.inbox-pane-effort,
.status-invited,
.status-archived,
.numbering-inherit-note {
  color: var(--text-quiet);
  font-size: 12px;
}

/* Search fields tab -- REAL checkboxes now (they used to render disabled). */
.search-fields-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.search-field-opt { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }

/* Active-filter chips below the toolbar. */
.filter-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.fchip {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 8px;
  border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
  font-size: 12px; color: var(--text); text-decoration: none;
}
.fchip:hover { border-color: var(--accent); color: var(--accent); }
.fchip-x { color: var(--text-quiet); font-size: 13px; line-height: 1; }

.column-picker-hint { font-size: 11px; color: var(--text-quiet); margin: 0 0 8px; }
.column-picker-list { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.column-picker-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; }
.column-picker-vis { flex: 1; display: flex; align-items: center; gap: 8px; cursor: pointer; }
/* Drag-to-reorder grip (⋮⋮). The locked Case/Subject row gets a same-width
   spacer instead so its checkbox/label stay aligned with the draggable rows. */
.col-drag { cursor: grab; color: var(--text-quiet); font-size: 14px; line-height: 1; user-select: none; padding: 0 2px; touch-action: none; }
.col-drag:active { cursor: grabbing; }
.column-picker-item.col-dragging { opacity: 0.5; }
.column-picker-lock-hint { font-size: 10px; color: var(--text-quiet); text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap; }
.column-picker-locked .column-picker-ord { opacity: 0.5; }
/* Input-shaped, NOT a card frame: --radius-sm (pixel-identical 6px), never
   --radius-md — these sit beside plain inputs on the same row and a 2px kink
   between same-species controls is worse than the drift being fixed
   (ui-framework ruling, 2026-08-22). */
.column-picker-ord { width: 48px; padding: var(--space-2) var(--space-3); font-size: var(--fs-2); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
.column-picker-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.column-picker-count { font-size: 11px; color: var(--text-quiet); }
.column-picker-buttons { display: inline-flex; align-items: center; gap: 6px; }
.column-picker-buttons .btn { padding: 5px 12px; font-size: 12px; }

/* ---- Saved views: now the Settings dialog's "Views" TAB, so the panel is
   layout-neutral (the dialog owns the popover chrome). ---- */
.saved-views-menu { display: block; }
.saved-view-active-label { font-size: 12px; color: var(--text-quiet); margin: 0 0 8px; }
.saved-view-list { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.saved-view-row { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; font-size: 13px; }
.saved-view-row:hover { background: var(--accent-tint); }
.saved-view-active { background: var(--accent-tint); }
.saved-view-apply { flex: 1; color: var(--text); text-decoration: none; }
.saved-view-apply:hover { color: var(--accent-text); }
.saved-view-author { font-size: 11px; color: var(--text-quiet); }
.saved-view-delete-form, .saved-view-default-form { margin: 0; line-height: 0; }
.saved-view-delete { border: none; background: none; color: var(--text-quiet); cursor: pointer; font-size: 12px; padding: 0 2px; }
.saved-view-default { border: none; background: none; color: var(--text-quiet); cursor: pointer; font-size: 13px; padding: 0 2px; line-height: 1; }
.saved-view-default:hover { color: var(--amber); }
.saved-view-default.is-default { color: var(--amber); }
.saved-view-empty { font-size: 12px; color: var(--text-quiet); padding: 4px 6px; }
.saved-view-clear { display: block; font-size: 12px; color: var(--accent-text); text-decoration: none; margin: 0 0 8px; }
.saved-view-save { display: flex; align-items: center; gap: 6px; border-top: 1px solid var(--border); padding-top: 10px; }
.saved-view-name { flex: 1; padding: 5px var(--space-4); font-size: var(--fs-2); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
.saved-view-save-btn { padding: 5px 12px; font-size: 12px; }

.table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
.table th { text-align: left; color: var(--text-quiet); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; padding: 8px; border-bottom: 1px solid var(--border); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.table td { padding: 10px 8px; border-bottom: 1px solid var(--border); vertical-align: middle; }

/* Column alignment: the header follows the body so a column is never ragged.
   Right-align is for numeric/short-token columns, centre for status-like ones. */
.table th.ta-right, .table td.ta-right { text-align: right; }
.table th.ta-center, .table td.ta-center { text-align: center; }
/* Digits line up when a numeric column is right-aligned. */
.table td.ta-right { font-variant-numeric: tabular-nums; }

/* Sticky header: column labels stay visible while a long list scrolls. The
   background is opaque so rows can't show through (it matches --bg because the
   list renders straight into .content, not into a card), and the bottom rule is
   drawn with an inset shadow — a sticky <th>'s own border scrolls away in some
   engines. z-index 3 beats the selection cells' 2, which would otherwise paint
   the row checkboxes over the pinned header (equal z-index resolves by DOM
   order, and <td>s come later). */
.table-sticky-head thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg);
  box-shadow: inset 0 -1px 0 var(--border);
  border-bottom: none;
}

/* ---- The data-table FRAME ----
   Toolbar + filter chips + scrollable table + footer as ONE bordered surface,
   so the table reads as a single component instead of three things stacked on
   the page background. The toolbar is an attached header strip and the footer
   an attached base; only the middle scrolls. */
.data-table-frame {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  /* NOT overflow:hidden. That clipped the scroll region to the rounded corners,
     but it also trapped every popover opened from the toolbar INSIDE the frame —
     on a short table the Settings > Columns panel lost 312px off the bottom
     (owner report 2026-07-26). Clipping now lives on the scroll region itself,
     which is the only child that actually needs it. */
  overflow: visible;
}
/* Round whichever element sits at the frame's bottom edge, so square corners
   don't poke through now that the frame no longer clips. */
.data-table-frame > .list-footer { border-radius: 0 0 9px 9px; }
/* Footer becomes a two-part bar once paging is possible: the range on the left,
   the pager on the right. */
.list-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.list-pager { display: inline-flex; align-items: center; gap: 6px; }
.list-pager-btn { padding: 5px 12px; font-size: 12px; }
.list-pager-btn.is-disabled { opacity: 0.45; pointer-events: none; }
.list-pager-page { font-size: 12px; color: var(--text-quiet); padding: 0 4px; }
@media (max-width: 768px) {
  /* Thumb-sized pager controls, same 44px rule as the toolbar. */
  .list-pager-btn { min-height: 44px; display: inline-flex; align-items: center; }
  .list-footer { justify-content: space-between; }
  /* On a phone the pager sat below ALL 200 cards, so changing page meant
     scrolling the entire list to reach it. Stick the footer to the bottom of the
     viewport WHEN paging is actually possible (data-truncated is set only then),
     so Next/Previous stay in reach; a single-page list keeps a normal static
     footer and loses nothing. */
  .data-table-frame > .list-footer[data-truncated] {
    position: sticky;
    bottom: 0;
    z-index: 4;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.10);
    padding-bottom: max(8px, env(safe-area-inset-bottom));
  }
}
.data-table-frame > .table-scroll:last-child { border-radius: 0 0 9px 9px; }
.data-table-frame > .table-scroll { overflow: auto; }
.data-table-frame > .list-toolbar {
  margin: 0;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.data-table-frame > .filter-row { margin: 0; padding: 8px 12px; border-bottom: 1px solid var(--border); }
.data-table-frame > .list-footer {
  margin: 0;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
/* Header/body separation: a faint tint so the label row reads as a header band
   rather than a first data row (both were --surface white before). Also the
   sticky background, so pinned labels stay opaque over scrolling rows. */
.data-table-frame .table thead th,
.data-table-frame .table-sticky-head thead th { background: var(--bg); }
/* The last row's rule would double up with the footer border. */
.data-table-frame .table tbody tr:last-child td { border-bottom: none; }
/* Align the table's outer edges with the toolbar/footer text (12px), instead of
   sitting flush against the frame border — measured misalignment was 291 vs 303. */
.data-table-frame .table th:first-child,
.data-table-frame .table td:first-child { padding-left: 12px; }
.data-table-frame .table th:last-child,
.data-table-frame .table td:last-child { padding-right: 12px; }
/* The standalone empty state carries its own dashed box; inside the frame that
   would be a card within a card. Strip the border and let the frame hold it. */
.data-table-frame .empty-state {
  border: none;
  border-radius: 0;
  padding: 36px 12px;
}

/* Horizontal scroll when the columns can't fit. The table carries its own
   min-width (sum of its seed column widths), so the region scrolls exactly when
   the columns stop fitting — and the PAGE never scrolls sideways. Focusable so
   keyboard users can scroll it. */
/* The scroll region owns BOTH axes. Horizontal is the point (columns that don't
   fit), but it must also be able to scroll vertically, because `position: sticky`
   on the header resolves against its nearest scrolling ancestor — and this is it.
   With `overflow-x: auto` the spec forces `overflow-y: visible` to compute to
   `auto` anyway, so a header inside a region that never scrolls vertically could
   never stick (measured: sticky was scoped to .table-scroll, scrollable=false —
   the feature was a silent no-op). Capping the height makes the region scroll,
   which is what makes the sticky header real and keeps the frame's toolbar and
   footer in view on a long list. */
/* DO NOT "fix" the vertical scrollbar with `overflow-y: clip`. It looks like the
   escape hatch — the spec's visible->auto coercion above applies to `visible`,
   not to `clip`, so `overflow-x: auto; overflow-y: clip` really does produce a
   horizontal-only box and really does remove the phantom vertical scrollbar. It
   is still wrong, for two reasons (round-table 2026-07-26, .roundtable/list-
   scroll-architecture.md, decision D5):
     1. `clip` generates NO scrollport, so anything past the cap is unreachable
        by wheel, trackpad or keyboard. This region is deliberately focusable
        (role="region" tabindex="0", data-table.ts) precisely so keyboard users
        can reach it; `clip` silently guts that.
     2. At 200% browser zoom (WCAG 1.4.4) far fewer rows fit in the same cap, and
        with no scrollport the overflowing rows are simply GONE rather than
        scrolled — data loss that only appears for the users least able to
        tolerate it.
   It also does not rescue the sticky header: the box remains a scroll container
   with a fixed-height scrollport either way. There is no version of this that
   buys anything. */
.table-scroll { overflow: auto; max-width: 100%; max-height: 70vh; overscroll-behavior: contain; position: relative; }

/* Edge affordance: a scrollable table looks identical to a complete one, so a
   soft shadow marks where content continues. Only shown while there IS more in
   that direction (attributes set by scrollHintScript), and the LEFT one only
   after scrolling, so a table at rest shows no false hint. The shadows sit on
   the FRAME (not inside the scroll region) so they stay put while content moves. */
.data-table-frame { position: relative; }
.data-table-frame::before,
.data-table-frame::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 24px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease-out;
  z-index: 5;
}
.data-table-frame::after { right: 0; background: linear-gradient(to left, rgba(0, 0, 0, 0.13), transparent); }
.data-table-frame::before { left: 0; background: linear-gradient(to right, rgba(0, 0, 0, 0.13), transparent); }
.data-table-frame[data-more-right]::after { opacity: 1; }
.data-table-frame[data-scrolled]::before { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .data-table-frame::before,
  .data-table-frame::after { transition: none; }
}
.table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- Pinned identity column (horizontal scroll) ----
   Scrolling a wide table sideways used to carry the case number + subject out of
   view, leaving anonymous rows (observed at tablet width on the CR list). The
   selection box and the identity column now stay pinned. Backgrounds must be
   OPAQUE or the scrolling cells show through; z-index keeps them above ordinary
   cells but below the sticky header row (which is 3). */
.table-sticky-col .table-select-col,
.table-sticky-col .table-select-cell { position: sticky; left: 0; z-index: 2; background: var(--surface); }
.table-sticky-col td[data-sticky-col],
.table-sticky-col th[data-sticky-col] {
  position: sticky;
  left: var(--sticky-col-left, 0);
  z-index: 2;
  background: var(--surface);
}
/* Header cells of pinned columns sit at both stickies at once — keep them above
   the body's pinned cells. */
.table-sticky-col.table-sticky-head thead th[data-sticky-col],
.table-sticky-col.table-sticky-head thead .table-select-col { z-index: 4; background: var(--bg); }
/* Inside the frame rows are --surface; selected/hovered rows must keep their own
   fill on the pinned cells too, or the pin looks detached from its row. */
.data-table-frame .table-sticky-col tbody tr.is-selected td[data-sticky-col],
.data-table-frame .table-sticky-col tbody tr.is-selected .table-select-cell { background: var(--accent-tint); }
.data-table-frame .table-hoverable.table-sticky-col tbody tr:hover td[data-sticky-col],
.data-table-frame .table-hoverable.table-sticky-col tbody tr:hover .table-select-cell { background: var(--surface-hover); }
/* A hairline marking where the pinned region ends, visible only once scrolled. */
.table-sticky-col td[data-sticky-col]::after,
.table-sticky-col th[data-sticky-col]::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 1px;
  height: 100%;
  background: var(--border);
}

/* Optional row treatments (off unless the caller enables them). */
.table-striped tbody tr:nth-child(even) { background: var(--surface-hover); }
.table-hoverable tbody tr:hover { background: var(--surface-hover); }

/* Selected rows: the whole row reads as selected, not just its checkbox. The
   accent tint sits above the hover fill so a hovered selected row stays
   selected-looking. */
.table tbody tr.is-selected,
.table-hoverable tbody tr.is-selected:hover { background: var(--accent-tint); }

/* Keyboard focus for row navigation. The row is a stretched-link, so the
   focusable element is the case anchor inside it — draw the ring around the
   whole ROW rather than the (visually small) anchor, which is what the user is
   actually moving through. :has() degrades gracefully: without support the
   anchor keeps the UA outline. */
.table tbody tr:has(.stretched-link:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 4px;
}
.table tbody .stretched-link:focus-visible { outline: none; }

/* Sortable headers must read as HEADERS, not as links: inherit the header
   colour instead of the global anchor accent, no underline, and a focus ring
   drawn INSIDE the cell (`.table th` clips overflow, so an outset ring would be
   cut off once a column is dragged narrow). */
.table-sort {
  color: inherit;
  text-decoration: none;
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;
}
.table-sort:hover { color: var(--text); text-decoration: none; }
.table-sort.active { color: var(--text); }

/* Sort affordance: a sortable column that is NOT the active sort used to render
   no glyph at all, so "sortable" was indistinguishable from "not sortable" until
   you clicked and found out. The neutral ⇅ sits faint at rest so it never
   competes with the active direction, and strengthens on hover/focus. */
.sort-arrow { margin-left: 4px; font-size: 9px; opacity: 0.35; }
.table-sort:hover .sort-arrow,
.table-sort:focus-visible .sort-arrow { opacity: 0.8; }
.sort-arrow.is-active { opacity: 1; color: var(--accent-text); font-size: 8px; }
.table-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 3px; }
/* Fix 11 (ux-cycle1) */
.table-row-link { position: relative; cursor: pointer; }
.table-row-link:hover { background: var(--surface-hover); }
.table-cell-ticket { width: 100%; max-width: 0; }
.table-case-row { display: flex; align-items: center; gap: var(--space-2); }
.table-case-label { display: block; font-weight: 600; font-size: 12px; color: var(--text-quiet); }
/* Migrated chip in a dense table row (2026-08-18): the base .chip's 11px/9px
   padding was sized for a standalone badge, not a second element sharing a
   line with the case number — .table-case-row now keeps them on one line,
   this just quiets the chip itself to match the row's own scale. */
.chip-compact { padding: 1px 6px; font-size: 9px; font-weight: 500; }
.table-case-label.stretched-link::after { content: ''; position: absolute; inset: 0; }
.table-subject-text {
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* ---- Phase 2: bulk selection column ---- */
/* A narrow leading column carrying a checkbox. It must sit ABOVE the row's
   stretched-link overlay (.table-case-label::after covers the whole <tr>), so
   the cell is positioned with a z-index — a click lands on the checkbox and
   selects, instead of falling through to the row's navigation anchor. */
/* The ONE definition of the select column's width. data-table.ts's sticky
   offset resolves to this same variable (--sticky-col-left), because it used to
   carry its own hard-coded 44 against this 34 — a 10px disagreement that pushed
   the pinned identity column over its neighbour and clipped "STATUS" to
   "TATUS" on the incident and CR lists. */
:root { --table-select-col-w: 34px; }
.table-select-col { width: var(--table-select-col-w); }
.table-select-col,
.table-select-cell { position: relative; z-index: 2; text-align: center; padding-left: 8px; padding-right: 4px; }
.table-select-cell input,
.table-select-col input { cursor: pointer; margin: 0; }

/* ---- Phase 2: bulk action bar (hidden until >=1 row selected) ---- */
.bulk-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  padding: 8px 12px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--accent-tint);
}
.bulk-bar[hidden] { display: none; }
.bulk-bar-count { font-size: 13px; font-weight: 600; color: var(--text); }
.bulk-bar-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ---- Phase 2: batch-result banner ---- */
/* Rendered from the bulk-assign flash. Reuses .refusal-note for each failure
   line; the panel tone shifts by outcome (all-ok green / all-fail red /
   partial amber), built from existing tokens so it reads correctly in both
   themes. */
.batch-result {
  margin-bottom: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}
.batch-result-ok { border-color: var(--green); background: var(--green-tint); }
.batch-result-fail { border-color: var(--red); background: var(--red-tint); }
.batch-result-partial { border-color: var(--amber-border); background: var(--amber-tint); }
.batch-result-summary { font-size: 13px; font-weight: 600; color: var(--text); }
.batch-result-failures { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.batch-result-line { display: flex; gap: 6px; align-items: baseline; }
.batch-result-case { font-size: 12px; font-weight: 700; color: var(--text-quiet); }
/* Was borrowing .refusal-note's red-text look by class name alone — that
   class retired 2026-08-02 with the JS mechanism it was named for (see
   .toast above), but this styling need (a per-row failure line, unrelated
   to htmx:responseError) is real and stays; folded directly in here. */
.batch-result-msg { margin: 0; color: var(--red); font-size: 12px; }

/* ---- Phase 4: row density (comfortable | compact) ---- */
/* Compact tightens padding + drops the font a step; pure presentation, toggled
   client-side via the icr2_density cookie. Comfortable is the base .table. */
.table-compact { font-size: 12px; }
/* Compact buys its density from PADDING, not from shrinking the label below the
   point where uppercase 600 stays legible — 10px was under that floor (UX review
   2026-07-25). The header keeps 11px; the rows get tighter instead. */
.table-compact th { padding: 4px 8px; font-size: 11px; }
.table-compact td { padding: 4px 8px; }
.table-compact .table-case-label { font-size: 11px; }

/* ---- Phase 4: column resize (rendered only when icr2_resize=on) ---- */
/* The base .table is already table-layout:fixed; in resizable mode the header
   cells carry explicit inline widths, so the flexible ticket cell must yield to
   them instead of forcing width:100%. */
.table-resizable th { position: relative; }
.table-resizable .table-cell-ticket { width: auto; max-width: none; }
/* The grab zone must STRADDLE the column boundary (users aim at the visible
   edge), which needs the header to stop clipping — .table th sets
   overflow:hidden for label ellipsis, so re-open it only on resizable tables. */
.table-resizable th { overflow: visible; }
.col-resizer {
  position: absolute;
  top: 0;
  right: -4px;
  width: 9px;
  height: 100%;
  cursor: col-resize;
  user-select: none;
  touch-action: none;
  z-index: 1;
}
/* Widen the browser's hairline hit target to a grabbable zone, and show a clear
   accent rule on hover so the drag affordance is discoverable. */
.col-resizer::after {
  content: '';
  position: absolute;
  top: 20%;
  right: 3px;
  width: 2px;
  height: 60%;
  background: var(--border);
}
.col-resizer:hover::after { background: var(--accent); top: 0; height: 100%; }
/* While a drag is in flight the whole document shows the resize cursor and
   suppresses text selection (set/cleared on <body> by the resize script). */
body.col-resizing { cursor: col-resize; user-select: none; }

/* ---- Mobile ticket cards (<=768px) ---- */
/* On desktop/tablet the list renders its data table; the stacked cards are
   hidden. At <=768px the table (and its desktop-only column/density/resize
   controls) hide and the cards take over. Scoped to .ticket-list-responsive so
   only renderTicketListBody's lists flip — tables rendered directly elsewhere
   (My Cases, /approvals) are untouched. */
/* It's a <ul>, so the UA's default list padding applied: measured 71px of inset
   on the LEFT against 31px on the right at 390px — a visible 40px lopsidedness
   (owner report 2026-07-26, "margin on left and right are too wide"). Reset the
   list box so the cards sit symmetrically inside the frame. */
.ticket-cards { display: none; list-style: none; margin: 0; padding: 0; }
.ticket-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
.ticket-card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.ticket-card-case { font-size: 12px; font-weight: 700; color: var(--text-quiet); letter-spacing: 0.02em; }
.ticket-card-subject { font-size: 14px; font-weight: 600; color: var(--text); line-height: 1.35; }
.ticket-card-meta { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 12px; color: var(--text-soft); }
.ticket-card-assignee { color: var(--text-quiet); }

@media (max-width: 768px) {
  /* The table is wrapped in .table-scroll (horizontal-scroll region), so it is
     no longer a DIRECT child here — matching only `> table.table` silently
     stopped hiding it and mobile rendered the table AND the cards. Hide the
     wrapper too, and keep the direct-child form for tables rendered without it. */
  .ticket-list-responsive > .table-scroll,
  .ticket-list-responsive > table.table { display: none; }
  .ticket-list-responsive > .ticket-cards { display: flex; flex-direction: column; gap: 10px; }

  /* The whole action cluster used to be display:none here, which suppressed FOUR
     capabilities (saved Views, Filters, Search fields, Export) to hide the TWO
     that genuinely don't apply to cards — a mobile user simply could not switch
     view, filter, or export. Keep the cluster; hide only what cards ignore. */
  .list-toolbar { flex-wrap: wrap; row-gap: 8px; }
  .list-toolbar-actions { margin-left: auto; }
  /* Now that the actions are icon-only they fit beside the search field —
     forcing search onto its own row cost a whole 45px band for two glyphs. */
  .list-search { flex: 1 1 auto; min-width: 0; }
  .list-search input[type="search"] { min-width: 0; flex: 1; }

  /* Reclaim horizontal room: 16px each side plus the frame border plus the card
     padding left only ~288px of a 390px screen for actual content. */
  .content { padding: 16px 10px; }
  .data-table-frame > .list-toolbar { padding: 10px; }
  .ticket-card { padding: 12px; }


  /* Icon-only toolbar on phones: labels cost ~60% of the row width and the
     glyphs are already unambiguous next to a search field. */
  .list-toolbar .btn-label { display: none; }
  .list-toolbar .btn-icon-only { display: inline-flex; }
  /* Touch targets: measured 35x32 for the icon buttons and 31px chips — under
     the ~44px comfortable minimum, so mis-taps were likely. Height comes up to
     44 and icon-only controls get a square footprint. The row grows a few px;
     that is the right trade against tapping the wrong control. */
  .list-toolbar .btn,
  .list-toolbar summary.btn-quiet {
    min-height: 44px;
    min-width: 44px;
    justify-content: center;
    padding: 0 10px;
  }
  .list-search input[type="search"] { height: 44px; }
  .quick-view { min-height: 40px; display: inline-flex; align-items: center; }
  .btn-new { min-height: 44px; padding: 0 18px; }

  /* Quick views: five presets wrapped onto two rows and ate vertical space.
     One horizontally scrollable rail keeps every count visible and one tap away
     (a dropdown would hide both the counts and the current selection). */
  .quick-views {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding-bottom: 2px;
    scroll-snap-type: x proximity;
  }
  .quick-views::-webkit-scrollbar { display: none; }
  .quick-view { flex: 0 0 auto; scroll-snap-align: start; }
}

/* ---- Person avatar (initials, inline before reporter/assignee names) ---- */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent-tint);
  /* UX review 2026-07-25: --accent-dark has no dark binding — this measured
     2.17:1 on dark. --accent-on-tint is bound in both themes. */
  color: var(--accent-on-tint);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  margin-right: 6px;
  vertical-align: middle;
}

/* ---- Chips ---- */
/* padding stays 2px/9px, not var(--space-1)/var(--space-2): 2px matches
   --space-1 but 9px doesn't cleanly match any surveyed value (nearest are
   8px and 12px) — left literal rather than nudging the chip's horizontal
   padding a visible amount to force a token fit, per VISUAL-REFRESH-PLAN.md's
   "don't force it" rule. */
.chip { display: inline-block; padding: var(--space-1) 9px; border-radius: var(--radius-pill); font-size: var(--fs-1); font-weight: 600; }
/* Priority severity chips (Phase 1): Critical red, High amber, Medium neutral;
   Low/Planning render as quiet text (.priority-quiet, no chip). */
.chip-priority-critical { background: var(--red-tint); color: var(--red); }
/* .chip-numbering-lock groups here (Phase 5 chip-family merge, 2026-08-22):
   same amber fill, different MEANING — a lock note, not a priority — kept as
   its own name so neither can be restyled through the other by accident. */
.chip-priority-high,
.chip-numbering-lock { background: var(--amber-tint); color: var(--amber); }
.chip-priority-med { background: var(--chip-bg); color: var(--chip-text); }
.chip-workspace { background: var(--chip-bg); color: var(--chip-text); }
/* Provenance, not state — dashed border + quiet text deliberately distinct
   from chip-workspace/chip-status's solid fills, so "Migrated" never reads
   as a live status value (2026-08-03 Silicon Box import). */
.chip-migrated { background: transparent; color: var(--text-quiet); border: 1px dashed var(--border); }
/* .chip-ai and .saved-view-private-chip group here (Phase 5, 2026-08-22):
   identical accent fill, three distinct meanings (a CR cross-reference, an
   AI-suggested label, a private-view marker) — one declaration, several
   names, the .chip-status-* grouping shape. */
.chip-cr,
.chip-ai,
.saved-view-private-chip { background: var(--accent-tint); color: var(--accent-on-tint); }
.chip-status { background: var(--chip-bg); color: var(--chip-text); }
.chip-status-closed, .chip-status-deployed,
/* Invitation endings join the quiet group (2026-08-11 admin sweep). Every
   invitation status used to render the bare .chip-status grey, so on the
   Invitations screen a REVOKED invitation was visually identical to an
   ACCEPTED one — the glyph was the only difference, on the very screen whose
   list is mostly spent rows. These reuse the app's existing status vocabulary
   rather than inventing an admin-only one. */
.chip-status-revoked, .chip-status-expired { background: var(--chip-bg); color: var(--text-quiet); }
/* Fix 10 (ux-cycle1): header role-switcher pills had no color binding at
   all (only .chip's bare shape rules applied), so they stayed native
   button gray/black regardless of theme. */
.chip-demo { background: var(--chip-bg); color: var(--chip-text); border: none; cursor: pointer; }
.chip-status-cancelled, .chip-status-rejected { background: var(--red-tint); color: var(--red); }
.chip-status-solution_proposed, .chip-status-buyoff, .chip-status-final_approval,
.chip-status-lead_approval, .chip-status-owner_review, .chip-status-uat { background: var(--amber-tint); color: var(--amber); }
.chip-status-resolved,
.chip-status-accepted { background: var(--green-tint); color: var(--green); }
/* The post head's "awaiting a decision" chip. Its own class, NOT
   .chip-status-uat: that is a STATUS rule shared with lead_approval and
   owner_review, so Project's "Pending decision" was being styled by a
   CR-UAT selector and restyling UAT status chips would silently restyle both.
   Same amber pair, stated once for a chip that means something else. */
/* ROUND 8 — the wireframe's `.chip.pending` is an OUTLINE in strong ink
   (`border-color:var(--ink); color:var(--ink); font-weight:600`), not an amber
   fill. Two reasons to follow it, and they agree.
   Round 5's: amber means ATTENTION in this app (.banner-warn, the DM's
   missing-evidence note). "Pending sign-off" is a fact about an open gate on a
   record that is behaving exactly as designed — spending the warning colour on
   it is how the warning colour stops meaning anything.
   And shape: round 2 made the type tag a SOLID fill, so a second filled chip
   on the same head line competes with the one mark that says what kind of post
   this is. Filled tag, outlined chip — two roles, two treatments.
   Only MS-30's feed uses this class (CR's "Pending sign-off", Project's
   "Pending decision"). The transport submission's own Pending pill is
   `.chip chip-status chip-status-pending` — a different class, and one with no
   rule of its own, so it resolves through `.chip-status`'s neutral grey. That
   is untouched here and is fine as it stands: a submission awaiting the DM is
   a queue state, not a gate the reader is being pointed at. */
.chip-pending {
  background: transparent;
  border: 1px solid var(--text);
  color: var(--text);
  font-weight: 600;
}

/* §15 A3 — "whose move is it", on the card that is waiting. Deliberately the
   quiet warn tint, not the accent: this is not an action the reader can take,
   it is a statement that the next act belongs to somebody else. An accent chip
   would read as a control. */
.chip-awaiting {
  /* --amber/--amber-tint/--amber-border: the product's existing waiting colour,
     already contrast-measured in both themes (4.76:1, B3). Invented fallbacks
     were the first draft here and are worse than useless — a token that does
     not exist means the fallback silently becomes the design, in one theme
     only, with nobody told. */
  background: var(--amber-tint);
  color: var(--amber);
  border: 1px solid var(--amber-border);
}
/* The one DOTTED-ring surface in the product (DEC-010, restyled 2026-08-10).
   Sponsor: "if user account doesn't exist ... can we use dot border instead to
   differentiate?" — dashed and the tokenfield's own borders were close enough
   at 1px that "not a login" read as just another chip. Dotted is legible as a
   deliberate difference at chip scale, where dashed was not. */
.chip-contact {
  background: transparent;
  border: 1px dotted var(--gray-500);
  color: var(--text-quiet);
}

/* Inbox (naming-ruling-2026-07-20 / ICR2 Inbox Semantics panel): the verb
   chip leads every row, a label only — never an inline mutation. */
/* UX review 2026-07-25 fixed --accent-dark-as-text everywhere it found it and
   missed this one. --accent-dark has no dark binding, so this measured 2.17:1
   on dark — on the chip that leads EVERY Inbox row, and that round 9 just made
   load-bearing by putting the gate name in it. --accent-on-tint is bound in
   both themes. */
.chip-verb { background: var(--accent-tint); color: var(--accent-on-tint); flex: none; }
.inbox-section + .inbox-section { margin-top: 24px; }
/* Two consumers, one rule: the notification row's age has a byte-identical
   requirement, so the selector is widened rather than the declaration copied. */
.inbox-row-age, .notif-row-age { color: var(--text-quiet); font-size: 11px; flex: none; white-space: nowrap; }
.inbox-row-context { color: var(--text-quiet); font-size: 12px; margin: 2px 0 0; }

/* ---- Inbox split-pane (.roundtable/inbox-splitpane.md, SCR Inbox Split
   Pane.md) — classic mail split: ~380px list column, fluid action pane.
   Progressive enhancement over the grouped list above (same rows, same
   .inbox-row/.inbox-section markup) — this block only adds the two-column
   shell + selection/pane/confirmation/stale states. ---- */
.inbox-split { display: flex; gap: 24px; align-items: flex-start; }
/* The zero-items branch has one child, not two — flex's shrink-to-fit
   default left the empty-state card narrow and stuck top-left instead of
   spanning the row like every other emptyState() caller (all of which sit
   in an ordinary block container, not a flex split-pane shell). */
.inbox-split-empty { display: block; }
.inbox-list {
  flex: none;
  width: 380px;
  max-height: calc(100vh - 160px);
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}
.inbox-list .inbox-section { padding: 0; }
.inbox-list .inbox-section + .inbox-section { margin-top: 0; }
.inbox-list .inbox-section-heading {
  position: sticky;
  top: 0;
  margin: 0;
  padding: 8px 14px;
  font-size: 11px;
  text-transform: uppercase;
  color: var(--text-quiet);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  z-index: 1;
}
.inbox-list .inbox-row { padding: 10px 14px; border-bottom: 1px solid var(--border); }
.inbox-list .inbox-row:last-child { border-bottom: none; }
/* Selection state: the panel's one new visual state, audited in both theme
   bindings below (PRN-05-equivalent dual-theme requirement) — a themed
   tint + accent left rule, never a bare color literal. */
.inbox-row[aria-selected='true'] {
  background: var(--accent-tint);
  box-shadow: inset 3px 0 0 var(--accent);
}

.inbox-pane-region { flex: 1; min-width: 0; }
.inbox-pane {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 20px 24px;
  box-shadow: var(--shadow-card);
}
.inbox-pane:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.inbox-pane-hint, .inbox-pane-stale { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; text-align: center; gap: 12px; }
.inbox-pane-header-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.inbox-pane-subject { margin: 10px 0 4px; font-size: 18px; }
.inbox-pane-meta { margin: 0 0 6px; color: var(--text-quiet); font-size: 12px; }
.inbox-pane-open-link { font-size: 12px; }
.inbox-pane-header { border-bottom: 1px solid var(--border); padding-bottom: 14px; margin-bottom: 14px; }
.inbox-pane-context { margin-bottom: 14px; }
.inbox-pane-history { background: var(--chip-bg); border-radius: 8px; padding: 10px 14px; margin-bottom: 10px; }
.inbox-pane-history h3 { font-size: 11px; text-transform: uppercase; color: var(--text-quiet); margin: 0 0 6px; }
.inbox-pane-history-row { margin: 0 0 4px; font-size: 13px; color: var(--text-quiet); }
.inbox-pane-rejection-reason { color: var(--red); }
.inbox-pane-stale-message { color: var(--text-quiet); }
.inbox-pane-stale-actions { display: flex; gap: 10px; }

/* Confirmation strip (Consensus: "the point of the feature") — a themed
   success tint, never a bare green literal. */
.inbox-confirmation-strip {
  background: var(--green-tint);
  color: var(--green);
  border-radius: 8px;
  padding: 10px 16px;
  margin-bottom: 12px;
  font-weight: 600;
  font-size: 13px;
}

/* Mobile push-pane (owner ruling, naming-ruling-2026-07-20 "Split-pane
   Inbox rulings" point 2: IN v1). Reuses the mobile-sidebar transform +
   prefers-reduced-motion recipe verbatim (see .sidebar above) — same
   pattern, a different layer. Below 768px the split collapses to a single
   column (the list); tapping a row slides #inbox-pane-region in as a
   full-screen layer via the .inbox-pane-open class on #inbox-split. */
@media (max-width: 768px) {
  .inbox-split { flex-direction: column; }
  .inbox-list { width: 100%; max-height: none; }
  .inbox-pane-region {
    position: fixed;
    inset: 0;
    z-index: 96;
    background: var(--bg);
    overflow-y: auto;
    padding: 16px;
    transform: translateX(100%);
    transition: transform 220ms ease;
  }
  .inbox-split.inbox-pane-open .inbox-pane-region { transform: translateX(0); }
  .inbox-mobile-pane-back { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 12px; }
}
@media (min-width: 769px) {
  .inbox-mobile-pane-back { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .inbox-pane-region { transition: none; }
}

/* ---- Ticket detail ---- */
/* The old two-column ticket-layout grid's mobile collapse rule lived here
   until 2026-08-22 — the markup that carried it was removed by MS-30 P4
   (ms30-p4-header.spec.ts asserts its absence) and the rule outlived it,
   kept alive only by its own pinning test. Both are gone together. (Class
   name deliberately not spelled here — a dead selector living on in prose
   is the Q-15c trap DESIGN-SYSTEM.md warns about.) */
.chat-stream { display: flex; flex-direction: column; gap: 10px; }
/* 2026-08-10: `.chat-stream` gained a `[hidden]` user when the expand dialog
   started carrying BOTH views (Activity moved into the ⋯ menu). A class that
   sets `display` overrides the browser's own `[hidden] { display: none }`, so
   the full view would have rendered VISIBLE while claiming to be hidden — the
   exact fault chat-history-render.spec.ts's structural guard exists to catch,
   and did, in the same commit that introduced it. */
.chat-stream[hidden] { display: none; }
.chat-event { font-size: 13px; }
/* 2026-07-23 follow-up #2 ("chat still look ugly?"): the earlier pass put
   the avatar INSIDE the bubble's meta row, stacked above the text — reads
   as a labeled card, not a chat message. Real chat apps (iMessage/Slack DM/
   WhatsApp) put the avatar BESIDE the bubble as a sibling, and mirror the
   whole row (avatar left+bubble-left-aligned vs. bubble-right-aligned+
   avatar right) so which side a message is on is legible from a glance
   at the row shape alone, not just the tint. Asymmetric corner radii give
   each bubble a "tail" toward its own avatar, the detail that reads as
   "considered" rather than "a rounded rectangle". */
/* font-size: bubbles were the only element in the stream still at the inherited
   body size while .chat-system (12px) and .chat-comment-meta (11px) sat below
   it. Set on .chat-message, NOT by adding a class in the TS — two specs pin
   `class="chat-message ` as the row's literal opening token. The expand
   dialog's .chat-expand-dialog .chat-comment-body (higher specificity) still
   wins, so zoom-to-read survives. */
.chat-message { display: flex; gap: 8px; align-items: flex-end; max-width: 100%; font-size: 13px; }
.chat-message--own { flex-direction: row-reverse; }
.chat-message-avatar { flex: none; }
.chat-message-bubble {
  background: var(--chat-bubble-other-bg);
  border: 1px solid var(--chat-bubble-other-border);
  border-radius: 14px 14px 14px 4px;
  padding: 8px 12px;
  max-width: calc(85% - 30px);
}
.chat-message--own .chat-message-bubble { background: var(--accent-tint); border-color: var(--accent-tint); border-radius: 14px 14px 4px 14px; }
/* Legacy shape: plain bubble, no avatar row. Reached only when a caller of
   the exported renderStreamEvents passes no viewerId — NOT "CR detail", as
   the old comment here claimed: cr-detail.ts and incident-detail.ts both
   fill viewerId from req.user.id and both go through chatStream, which
   passes it unconditionally. Kept as a defensive path for external callers;
   corrected 2026-07-28 (chat-history audit iter 1). */
.chat-comment:not(.chat-message-bubble) { background: var(--chat-bubble-other-bg); border: 1px solid var(--chat-bubble-other-border); border-radius: 10px; padding: 8px 10px; max-width: 85%; }
/* .chat-comment--own / --other rules were deleted 2026-07-28: nothing has ever
   emitted those classes (the viewer-aware shape uses .chat-message--own/--other
   instead), so they were styling markup that does not exist. */
.chat-system { color: var(--text-quiet); padding: 4px 10px; font-size: 12px; text-align: center; }
/* Exceptional rows (rejections, overrides, reopens, rule-cleared gates) step
   up to full --text and left-align, so a reader scanning a long stream can find
   them. A `.chat-action { color: --text-quiet }` alone would have been a visual
   no-op — .chat-system already paints the whole row --text-quiet — so the
   differentiation has to come from the modifier lifting the row, with the verb
   staying quiet inside it. */
/* The status folded onto its causing action. Deliberately quiet: the action is
   the news, the resulting status is context. */
.chat-status-suffix { color: var(--text-quiet); }
.chat-system--notable { color: var(--text); text-align: left; }
.chat-system--notable .chat-action { color: var(--text-quiet); font-weight: 400; }
.chat-actor { font-weight: 600; margin-right: 6px; }
.chat-comment-meta { display: flex; align-items: center; gap: 6px; font-size: 11px; }
.chat-comment-meta .chat-actor { font-size: 12px; }
.chat-comment-body { margin: 4px 0 0; white-space: pre-wrap; }
/* .chat-system is a plain block, so float works there. .chat-comment-meta is
   display:flex (above), where float does NOT apply — both the float:right
   here and the float:left override below were inert, which is why bubble
   timestamps sat jammed against the actor name instead of at the bubble's
   trailing edge. Split into a float rule for the block and a flex rule for
   the flex row (2026-07-28, chat-history audit iter 1). */
.chat-system time { float: right; color: var(--text-quiet); font-size: 11px; }
.chat-comment-meta time { margin-left: auto; color: var(--chat-bubble-meta); font-size: 11px; }
.chat-message--own .chat-comment-meta { flex-direction: row-reverse; }
/* Reset margin-left or the base `auto` above cancels the mirrored intent.
   margin-inline-start would not work — it follows `direction`, not
   `flex-direction`. */
.chat-message--own .chat-comment-meta time { margin-left: 0; margin-right: auto; }
/* "Message group by date" — a quiet centered label between day
   boundaries, the common chat-app pattern (WhatsApp/Slack-style). */
.chat-date-separator { text-align: center; color: var(--text-quiet); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; margin: 4px 0; }
/* 2026-07-23 follow-up: "make it scrollable" — the rail's default view is
   a fixed-height scroll container instead of growing the whole page;
   reverse:true (incident-detail.ts) means the newest item is already the
   first one visible with no scrolling needed to find it. */
.chat-stream-view[data-chat-view] { max-height: 360px; overflow-y: auto; }
/* The stream views and the expanded dialog stream are <ol> (2026-07-28: the
   activity stream is a list, and AT should announce its length and position).
   Without this reset a UA adds padding-inline-start: 40px and numeric markers
   in BOTH places. .chat-event-item is a bare wrapper — the row's own layout
   still lives on .chat-message / .chat-system inside it. */
.chat-stream-view, .chat-stream--expanded { list-style: none; margin: 0; padding: 0; }
/* list-style is also set per-item, not only on the <ol>: these <li>s are
   rendered by renderStreamEvents, which is exported and can be placed in a
   container that lacks the reset. A stray bullet beside the date separator is
   exactly what that looks like. */
.chat-event-item, .chat-empty-item, .chat-date-separator { list-style: none; }
/* Same shape as .table-scroll:focus-visible above, so these inherit a token
   that has already been contrast-audited in both bindings. */
.chat-stream-view:focus-visible, .chat-stream--expanded:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Activity count beside the heading — quiet, never competing with the h3. */
.chat-count { color: var(--text-quiet); font-weight: 400; font-size: 12px; }
/* Mobile (2026-07-28): no @media rule anywhere touched a .chat-* class, so the
   two stream controls stayed at their 30px desktop size — under the 44px
   minimum touch target. min-width/min-height (not width/height) is deliberate:
   it beats .icon-btn's width/height without a specificity fight. (This block
   was originally kept separate from the old ticket-layout grid's media block,
   which a spec test pinned; that rule and its test were retired 2026-08-22 —
   the block just stays as it is.) */
@media (max-width: 768px) {
  .chat-expand-btn { min-width: 44px; min-height: 44px; }
  .chat-activity-toggle { min-height: 44px; display: inline-flex; align-items: center; }
  .chat-stream-view[data-chat-view] { max-height: 50vh; }
}
.chat-stream-heading-actions { display: flex; align-items: center; gap: 6px; }
/* width/height dropped 2026-07-28: .icon-btn (later in this file, equal
   specificity) sets 30x30, so the 22px here never applied. Removed rather than
   given higher specificity — the button has rendered at 30px all along, and
   making it genuinely 22px now would be a silent visual change, not a fix. */
.chat-expand-btn { flex: none; padding: 0; color: var(--text-quiet); }
.chat-expand-btn:hover { color: var(--accent); background: var(--surface); }
/* "Zoom to see more detail" — the existing expand-to-modal convention
   (maximize-2, same as md-editor-field.ts), not literal pinch-zoom: a
   larger dialog with larger type for comfortable reading. */
.chat-expand-dialog .chat-stream--expanded { max-height: 65vh; overflow-y: auto; font-size: 15px; }
.chat-expand-dialog .chat-comment-body { font-size: 15px; }
/* 2026-07-23 follow-up #2: "on the pop up, can we have the comment box" —
   an inline comment form so the expanded view is a real place to reply
   from, not just a bigger read-only history. */
.chat-expand-dialog .comment-box { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }

.proposal-card { background: var(--accent-tint); border-radius: 8px; padding: 14px; border: none; }
.proposal-header { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.proposal-number { font-weight: 700; font-size: 12px; }
.proposal-text { margin: 0 0 10px; }
.proposal-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.proposal-actions form { display: flex; gap: 8px; align-items: center; margin: 0; }
.proposal-decision { font-weight: 600; font-size: 12px; }
.proposal-accepted { color: var(--green); }
.proposal-rejected { color: var(--red); }

.comment-box { display: flex; gap: 8px; margin-top: 12px; }
.comment-box textarea { flex: 1; }

/* Root Cause + Solution's shared hx-include target (2026-07-23 command bar
   tidy-up: "maintain Root Cause & Solution till click Propose Solution" —
   both fields, one commit action, now triggered from the command bar's
   Propose Solution button rather than an inline submit). This wrapper
   spans both .pref-cards it contains, so it needs the SAME flex/gap
   .reading-column already uses between siblings, or the 28px gap between
   Root Cause and Solution collapses. */
.fields-span-wrapper { display: contents; }

.rail { display: flex; flex-direction: column; gap: 18px; }
.rail-section h3 { font-size: 11px; text-transform: uppercase; color: var(--text-quiet); margin: 0 0 6px; }
.rail-section p { margin: 0 0 4px; }
.rail-timestamps p { font-size: 11px; color: var(--text-quiet); }

/* ---- DEC-032: Incident Detail reading layout ----
   A dedicated class family (NOT a repurposing of .chat-stream/.rail above,
   which other screens still use — the old ticket-layout grid, the third
   member this comment used to name, died with MS-30 P4 and was swept
   2026-08-22) — center reading column (eyebrow/h1/byline already in
   .ticket-header-editorial, then Description/Root Cause/Solution/Accept-
   Reject/closing status line/Attachments) + one consolidated, stacked
   right rail (compact metadata -> linked CRs -> chat), no tab-switcher.
   Owner feedback (2026-07-22): Attachments moved from the rail into the
   reading column, directly below the closing status line — reuses
   ticket-detail-shared.ts's attachmentsRailSection() markup unchanged
   (CR detail's rail still calls it the original way), restyled below to
   match the reading column's own heading convention instead of the rail's
   compact uppercase label.
   Mobile: reading column stacks ABOVE the rail (source order already puts
   .reading-column first, so the single breakpoint below just needs to
   collapse the grid to one column — never reorder). */
/* .incident-reading-layout REMOVED 2026-08-06. It was the two-column rail
   recipe; P4 retired the rail from all three detail screens and nothing has
   rendered the class since. DESIGN-SYSTEM.md kept it registered as "the rail
   recipe for anything that still wants one" — but a recipe with no dish is
   just a rule that will drift. Restore from git if a rail ever returns. */
@media (max-width: 768px) {
}
/* ---- MS-30 P4 (docs/TICKET-PAGE-REDESIGN.md §1): the SINGLE-column ticket
   page. The wireframe has no right-hand rail, and leaving one is what made the
   sponsor say twice that the redesigned screen "still shows the same" — the
   header fields were all still over there. `.incident-reading-layout` above is
   kept (nothing else should grow a 2-column ticket page, but the grid is still
   the documented recipe for a rail elsewhere); the three ticket detail screens
   now use this instead.
   The max-width is the point: a reading column that was 1fr of `1fr 300px` and
   is suddenly the whole viewport becomes an unreadable full-width line on a
   wide monitor. 900px keeps roughly the measure it had. Nothing here is a
   colour, so both themes are unaffected. */
.ticket-reading-single { display: flex; flex-direction: column; gap: 28px; padding-bottom: 20px; max-width: 900px; }
/* The header block (§2): qualifier line -> people row -> meta strip, stacked
   directly under the stepper and capped to the same measure as the column so
   the page reads as ONE column, not a header of a different width. */
/* Wireframe field boxes: each header VALUE reads as a field, not loose text —
   that is the single biggest reason the shipped header did not look like the
   drawing even once every row was present. Applies to the text-ish slots only;
   chips, selects and the people row bring their own shapes. */
.ticket-detail-field { display: block; border: 1px solid var(--border); border-radius: var(--radius-md); padding: 8px 10px; background: var(--surface); font-size: var(--fs-3); font-weight: 400; }
/* Header review §8 round 10 (2026-08-08): .ticket-subject-value is OUT of the
   boxed-field rule above. It is the READ rendering of the page <h1> (Project's
   title always; CR/Incident's whenever the viewer cannot edit the subject),
   and the box rule — a leftover from the retired round-6 label grid — was
   shrinking it to field size (fs-3) inside an input-shaped border, so the
   identity zone read as a form control while editors saw a 28px borderless
   title on the very same pages. The span now inherits the h1's own size and
   weight; nothing else is needed. */
.ticket-subject-value { display: inline; font-size: inherit; font-weight: inherit; }
.ticket-detail-field { max-width: 100%; }
/* Sponsor 2026-08-07 ("subject can be at header as before? better?"): the
   Subject is the page <h1> again, so the two rules that shrank it to field size
   INSIDE the header grid are gone with the grid row they served. The editable
   input goes back to .ticket-title-input's own 28px display size, in
   .ticket-header-editorial where .ticket-title has always lived. Deleted rather
   than left dead: a rule whose selector nothing matches is a trap for whoever
   next wonders why the title will not resize. */
.ticket-title { margin: 2px 0 0; }

/* ===== Header card redesign (§8 round 7, 2026-08-07 — wireframe round 7).
   The 2-column label grid (an 88px label track, every value boxed, all rows
   equal weight) read as a FORM. A record header reads as a DOCUMENT with
   editable properties (GitHub issue-fields strip; Attio record header):
     qualifier line → ONE flowing props strip (caps micro-label OVER value,
     groups flowing horizontally) → Detail as full-width body text →
     Attachment as a quiet chip row. The old grid's display:contents
     machinery went with the grid it served. */
.ticket-header-block { display: flex; flex-direction: column; gap: var(--space-5); max-width: 900px; margin-bottom: var(--space-5); }
/* The props strip: the People groups and the meta pairs FLOW TOGETHER in one
   wrapping row. Their own wrappers dissolve (display:contents) so each
   labelled group — .people-group and .header-prop alike — is a direct flex
   item of the strip. */
/* MS-30 §14 P3 — the header form (sponsor sketch 2026-08-10). One column of
   labelled rows: a quiet label in a fixed left column, the control to its
   right. This coexists with .ticket-header-props rather than replacing it —
   Incident renders fields, CR and Project still render the props strip, and
   both live inside the same .ticket-header-block. */
/* Sponsor 2026-08-10: *"size of name tag and field - can standardise? make it
   compact including dropdown as well."* One control height across the whole
   header form, so a chip, a tokenfield and a dropdown sit on the same baseline
   instead of three heights stacked down one column. 26px is the chip's own
   natural height, so the chip is the unit and everything else matches IT
   rather than a number invented here. */
.ticket-header-fields {
  --header-control-h: 26px;
  /* Sponsor 2026-08-10: "Editable field with some color - like light blue
     background on the field or border?".
     WHY IT WAS NEEDED. §8 rounds 7-9 stripped the header's boxes so it would
     read as a record rather than a form — which worked, and went one step too
     far: with Priority, System/Module and the tokenfields all sitting on plain
     surface, nothing said which facts you could change. "Looks like a document"
     and "looks read-only" turned out to be the same look.
     A TINT, not a border. Every control here already has a border or is
     deliberately borderless (the discreet selects), so adding outlines would
     re-box the header §8 spent three rounds un-boxing. A wash says "this is
     yours to change" without changing the shape of anything.
     Mixed toward --surface rather than using --accent-tint neat: neat, five
     of these stacked read as five selected rows. Both theme bindings inherit
     because the mix is computed from the live tokens, not a hardcoded blue. */
  --editable-tint: color-mix(in srgb, var(--accent-tint) 45%, var(--surface));
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
/* The editable controls. A READ-ONLY tokenfield keeps its dashed transparent
   treatment (.req-tokenfield--readonly, below) and must not pick this up —
   which is exactly the distinction the sponsor asked to be able to see. */
.ticket-header-field-control .req-tokenfield:not(.req-tokenfield--readonly),
.ticket-header-field-control select,
.ticket-header-field-control input[type='text'],
.ticket-header-field-control input[type='date'] {
  background-color: var(--editable-tint);
}
/* Hover already meant "this is interactive" via --surface; with a tint at rest
   that reversal would read as switching OFF. Deepen instead. */
.ticket-header-field-control select:hover,
.ticket-header-field-control input[type='text']:hover,
.ticket-header-field-control input[type='date']:hover {
  background-color: color-mix(in srgb, var(--accent-tint) 70%, var(--surface));
}
/* The header's Detail body is editable too (click-to-edit), and it is the one
   editable surface with no box at all — so it takes the tint only on hover,
   where the existing dashed affordance already appears. A permanent wash behind
   the record's body text would undo §8 round 9. */
.ticket-header-detail-slot .rt-editor--display:hover {
  background-color: var(--editable-tint);
}
/* The tokenfield drops its standalone-card margins inside a form row: the row
   grid already owns the spacing, and the two together read as a gap nobody
   chose. It also shrinks to the shared height when it holds a single line. */
.ticket-header-field-control > .req-tokenfield,
.ticket-header-field-control .req-tokenfield {
  margin: 0;
  min-height: calc(var(--header-control-h) + 8px);
  /* Sponsor 2026-08-10: "Minimum Length of input fields (make it looks more
     organised)". The boxes were sized by their contents, so a row with one
     chip ended a third of the way across and the row below it two thirds —
     a ragged right edge down the whole card. A floor (not a fixed width: they
     still grow with their chips, and still wrap) gives the column an edge to
     line up on. */
  min-width: min(360px, 100%);
  padding: 3px 6px;
  gap: 4px;
}
/* Compact chips: the × was a 24px touch target sized for the rail card, where
   it sat alone. In a dense form it made every chip taller than the row. 18px
   keeps a real target (the chip's own padding extends the hit area) without
   setting the row height. */
/* EXCLUDED from the discreet recipe above: the tokenfield's input lives inside
   a box that already owns its padding, so the recipe's -6px pull would drag it
   off its own border. */
.ticket-header-field-control .req-tokenfield-input { margin-left: 0; padding: 2px; }
.ticket-header-field-control .req-chip { padding: 2px 4px 2px 8px; }
.ticket-header-field-control .req-chip-x { width: 18px; height: 18px; }

.ticket-header-field {
  display: grid;
  /* A fixed label column so the controls line up down the card — the whole
     reason the sketch is legible as a form. minmax(0,1fr) lets a tokenfield
     shrink instead of forcing the card wider than the page. */
  grid-template-columns: 120px minmax(0, 1fr);
  align-items: center;
  gap: var(--space-4);
}
/* Rows whose control is a multi-line box (a tokenfield that has wrapped) read
   wrong centred — the label floats mid-way down a tall box. */
.ticket-header-field--top { align-items: start; }
.ticket-header-field--top > .ticket-header-field-label { padding-top: 7px; }
.ticket-header-field-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-quiet);
}
.ticket-header-field-control { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
/* Below the phone breakpoint a 120px label column leaves nothing for the
   control, so the row stacks — label over value, which is the props strip's
   own grammar and needs no new vocabulary. */
@media (max-width: 640px) {
  .ticket-header-field { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
  .ticket-header-field--top > .ticket-header-field-label { padding-top: 0; }
}
.ticket-header-props { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-5) 26px; }
/* One labelled property: caps micro-label stacked over its value. */
.header-prop { display: inline-flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
/* People groups take the SAME stacked label-over-value shape inside the
   header strip (the popover's lane variant keeps its own layout). Their
   labels also take the meta-strip-label's caps treatment — one strip, one
   label grammar; "Reporter" in sentence case beside "PRIORITY" in caps
   read as two different systems. */
.ticket-header-props .people-group-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-quiet); }
/* The "+ People" trigger is a peer of the VALUES, not the labels — bottom-
   aligned so it sits on the chips' line rather than floating at cap height. */
/* §8 round 23: the "+ People" trigger inside the header reads as the
   wireframe's .add-people affordance — a dashed quiet pill ("this adds
   something", distinct from the solid person chips beside it) — instead of
   the stock bordered button, which read as one more form control in the
   strip. The <summary class="btn btn-quiet btn-small"> markup is untouched;
   this restyles it in header context only. */
/* Detail is the record's BODY — full-width text, no label, no box at rest
   (the click-to-edit widget brings its own editor chrome on interaction). */
.ticket-header-detail-slot .ticket-detail-field { border: none; background: transparent; padding: 0; }
/* Attachment: a quiet full-width row opened by a dashed rule, per the
   wireframe's round-7 drawing. §8 round 22: laid out as ONE flex line —
   chips + trigger left, the "or drop a file here" hint pushed to the row's
   far right (the wireframe's .hx-attach .hint{margin-left:auto} grammar);
   inline flow left the hint dangling mid-row after the button, which read
   as an unfinished sentence rather than a quiet affordance. */
.ticket-header-attach-slot { border-top: 1px dashed var(--border); padding-top: var(--space-4); }
.ticket-header-attach-slot,
.ticket-header-attach-slot .header-droparea {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.ticket-header-attach-slot .header-droparea { flex: 1 1 auto; }
.ticket-header-attach-slot .attach-drop-hint { margin-left: auto; font-size: 10.5px; }
/* §8 round 25 (GATE-19 phase 1): the header card's ACTIONS slot — the intake
   stage's own record-level action (Incident's Submit) closes the card. Only
   actions whose eligibility window is contained in the card's open window
   belong here (see ticketHeaderBlock's slot doc). */
.ticket-header-actions-slot { display: flex; align-items: center; gap: var(--space-3); padding-top: var(--space-2); }
/* Sponsor 2026-08-10: "Update or Save button by card (appear next to action
   button when there is a change to card)".
   HIDDEN UNTIL DIRTY, with no JavaScript: the rich-text editor already sets
   `.rt-dirty` on its own wrapper, so `:has()` asks the card "does anything
   inside you have unsaved changes?" — one source of truth for that question,
   and it is the field's own rather than a second tracker that can disagree.
   A `display:none` default (not visibility) so it takes no space and the action
   button does not shift sideways when the card becomes dirty. */
.ticket-header-save, .ticket-header-dirty { display: none; }
/* .cc-card joins .ticket-header-card here (sponsor 2026-08-12: "save button
   should control the whole card .. pls apply the same way for all cards").
   Every communication card — Requirement, Proposal, Development, UAT note,
   project stage notes — now carries the Detail card's own row, gated on the
   same single source of truth: whether a field inside it is dirty. */
.ticket-header-card:has(.rt-dirty) .ticket-header-save,
.cc-card:has(.rt-dirty) .ticket-header-save,
.cc-card:has(.card-field-dirty) .ticket-header-save { display: inline-flex; }
/* The card's row reads as one sentence when something is unsaved: what is
   true, then the two things you can do about it. Same :has() gate, same single
   source of truth — the field's own dirty state. */
/* .card-field-dirty is the SAME question asked of a plain control (the Root
   Cause Type select) — see renderCardDirtyFieldScript. Listed beside .rt-dirty
   rather than tracked separately so "is anything unsaved in this card" has one
   answer whatever kind of control raised it. */
.ticket-header-card:has(.rt-dirty) .ticket-header-dirty,
.cc-card:has(.rt-dirty) .ticket-header-dirty,
.cc-card:has(.card-field-dirty) .ticket-header-dirty {
  display: inline-flex;
  align-items: center;
  font-size: 12px;
  color: var(--amber);
  margin-right: auto;
}
/* The record-level ⋯ overflow, rehomed from the command bar into the
   page-title actions row (never collapses; solves REJECTED). Reuses
   .card-menu's disclosure grammar; this only sizes the trigger to sit
   beside the status chip. Default drop-DOWN positioning is correct here —
   .command-bar-more's open-upward override retired with the bar seat. */
.header-more-menu > summary {
  font-size: 16px;
  line-height: 1;
  padding: 2px 8px;
  border-radius: 6px;
}
.header-more-menu > summary:hover { background: var(--surface-hover); color: var(--text); }
/* §8 round 26 (GATE-19 phase 2): the two rehomed action rows. Same recipe —
   a quiet explanatory note left, the primary action right — the shape of a
   decision band without its band chrome (the propose footer sits INSIDE the
   Solution card's body under its editor; the close band rides the Resolution
   card's real decision slot and inherits that band's container). */
.solution-propose-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.solution-propose-footer { margin-top: var(--space-4); }
/* §8 round 27 (GATE-19 phase 3): the Development checklist card's footer
   band — the record's IN_DEVELOPMENT→UAT advance (GitHub merge-box shape:
   the checklist above is the readiness display, this is its resolution).
   Same note-left/action-right recipe as the rehomed rows above. */
/* §8 round 27 (GATE-19 phase 3): the terminal-state banner — a CLOSED or
   CANCELLED incident's one live surface, under the (collapsed) header card.
   Quiet surface row: the fact and its window as dates for everyone, the
   Reopen trigger for the eligible. */
.terminal-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-5);
  color: var(--text-quiet);
  font-size: var(--fs-2);
}
/* MS-30 §7 (2026-08-07) — collapse/expand. Same disclosure grammar as
   .ticket-activity-details (native <details>, no script, state survives a
   body swap): open while the ticket/project is still in its INITIAL status,
   collapsed once it has advanced. The wrapped .ticket-header-block keeps its
   own margin-bottom, so this adds no extra spacing of its own when open.
   Round C (header review §8): the header is its own SECTION, closed by the
   wireframe's .h-block hairline (border-bottom) — visually distinct from
   the feed's post cards (bordered rounded boxes) without becoming a box
   itself. The hairline sits on the wrapper so it underlines the collapsed
   summary and the expanded block alike. */
/* Header review §8 round 18 — sponsor (2026-08-08): "can header be like a
   card style?" Reverses round C's hairline-section close: the header is now
   a QUIET WASH card — soft token-derived tint (color-mix of --text into
   --bg), --radius-lg, NO border, NO shadow. Feed post cards (.pref-card)
   are border-led --surface boxes that LIFT off the page; this card RECESSES
   into it in light and stays ringless in dark (where every post carries the
   --shadow-card border ring), so it reads as the ground the record sits on,
   not another post — preserving the round-C sponsor note that the header
   must differ from the feed's cards. The fill is computed from existing
   tokens, so it self-binds in both themes with zero new dark bindings.
   Do NOT add overflow:hidden here for corner cleanup: the header hosts
   typeahead popovers (.typeahead-list, z-index 5) that clipping would
   truncate. The --surface-hover line is a pre-color-mix fallback only; in
   light it equals --bg, degrading to the old unboxed look — functional,
   not broken. */
/* 2026-08-09: the wash lives on .ticket-header-card, worn by BOTH wrappers —
   the collapsible <details> and the standalone <div> the Incident DRAFT screen
   uses (a draft's card must not be collapsible: the fields in it are the whole
   point of the screen). Splitting the class off is what makes "same card, one
   of them doesn't collapse" expressible without a second copy of the wash. */
/* SPONSOR 2026-08-10: "can it be better same look and feel as other card?"
   
   Round 18 made this a quiet WASH card — no border, no shadow, recessed — on
   the reasoning that the header is the ground the record sits on rather than
   another post. Read back against the feed it sits above, that reasoning
   produced a grey bar: the one block on the page that did not look like the
   blocks under it, which is the opposite of "the ground".
   
   It takes the feed card's own chrome now — the same surface, border, radius
   and shadow as `.pref-card` — so a reader scanning the column sees one kind of
   object throughout. Same declarations, deliberately not `@extend`-style
   copying of values: if the feed card's chrome changes, this changes with it. */
.ticket-header-card {
  margin-bottom: var(--space-5);
  padding: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
/* Collapsed = the same card, slim: one summary line, tighter block padding,
   no trailing gap under the summary. Open keeps the roomier padding and the
   summary's existing var(--space-3) caption gap. */
.ticket-header-details:not([open]) {
  padding-block: var(--space-3);
}
.ticket-header-details:not([open]) > .ticket-header-summary {
  margin-bottom: 0;
}
/* Quiet text inside this card sits on the 4% wash, not on bare --bg, so it
   reaches for the wash-tuned token. Both themes are bound at the top of the
   file, which is why this is one line and not three: the earlier version
   restated the hexes here and had to keep two dark selectors in step by hand. */
.ticket-header-card { --text-quiet: var(--text-quiet-on-wash); }
.ticket-header-card .ticket-header-block { margin-bottom: 0; }
/* SPONSOR 2026-08-10: "design layout again to show detail information when
   collapse (looks and feels should be similar to other card)" + "make it a bit
   different from other card since it is header".

   Collapsed, this card used to stop being a card: one run of dot-separated
   values with nothing saying which was which, so "Critical", "MM" and a
   person's name — three different KINDS of fact — wore identical clothes and
   the reader decoded by position. The open card had already solved that with
   caps micro-labels over values; collapsing threw the grammar away and
   invented a second one.

   It is now a GRID: the disclosure marker in its own column, the content in
   the other, so a wrapping props row and the snippet line up under one another
   instead of hanging off the marker. Same `.header-prop` / `.meta-strip-label`
   pairs the open card uses — reused, not restyled, which is what makes the two
   states legibly one object.

   The "bit different" is carried by things that mean something rather than
   decoration: one dense wrapping row instead of a spaced strip, plain text
   where the open card has live controls, and the marker. A folded card, not a
   different component. */
.ticket-header-summary {
  cursor: pointer;
  font-size: var(--fs-2);
  font-weight: 600;
  color: var(--text-quiet);
  list-style: none;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--space-3);
  margin-bottom: 0;
}
/* The marker keeps column 1 beside the tag; everything else stacks in column 2,
   so the props and the snippet align under the tag rather than hanging off the
   marker. */
.ticket-header-summary::before { grid-row: 1; align-self: center; }
.ticket-header-summary-tag,
.ticket-header-summary-props,
.ticket-header-summary-detail { grid-column: 2; }
/* The tag on a card that has NO disclosure (before work starts — sponsor
   2026-08-10). Same badge, same place; it simply has no marker beside it and
   nothing to toggle. Spacing matches the open <details> summary so the card
   does not visibly change shape the moment work begins. */
.ticket-header-static-head { margin-bottom: var(--space-5); }
/* The tag is the card's heading and the thing that makes it read as a sibling
   of ROOT CAUSE / SOLUTION. `justify-self` so the inverted badge hugs its text
   instead of stretching the column. */
.ticket-header-summary-tag { justify-self: start; padding: 2px var(--space-3); }
.ticket-header-details[open] > .ticket-header-summary { margin-bottom: var(--space-5); }
.ticket-header-summary-props {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* Denser than the open card's props strip (26px): collapsed is a scan, not a
     read, and the labels already do the separating that spacing used to. */
  gap: 2px 18px;
}
/* Label over value inside a single line: the pair stacks like the open card's,
   just tighter, so the eye still binds key to value without a second row of
   whitespace per fact. */
.ticket-header-summary-props .header-prop { display: flex; flex-direction: column; gap: 0; }
.ticket-header-summary-props .meta-strip-value { font-weight: 500; color: var(--text); font-size: var(--fs-2); }
.ticket-header-summary::-webkit-details-marker { display: none; }
.ticket-header-summary::before { content: "▸"; font-size: 10px; }
.ticket-header-details[open] > .ticket-header-summary::before { content: "▾"; }
/* Header review §8 round 8 (2026-08-08): while the header is EXPANDED, the
   summary's person/priority/detail spans sit directly ABOVE the very rows
   they preview — the same fact twice within one viewport (worst on Project:
   "Owner: X" over an OWNER chip reading X). Collapsed they are the whole
   point; open they are noise. So open-state hides the content spans and the
   line reads as the section's caption instead — same static-title grammar
   as Activity's own <summary>. */
.ticket-header-details[open] > .ticket-header-summary > span:not(.ticket-header-summary-tag) { display: none; }
/* The `::after { content: "Details" }` that used to supply the open state's
   caption is gone: the tag is real text now, present in both states, so the
   word is selectable, translatable and readable by anything that does not run
   stylesheets — and there is one way of saying it instead of two. */
.ticket-header-summary:hover { color: var(--text); }
/* §8 round 12: age is the one span sitting directly beside the flex:1
   ellipsized Detail snippet, so it alone needs nowrap — the snippet does
   the shrinking, never the fact. */
/* §8 round 27 (GATE-19 phase 3): the assignee GAP token ("Unassigned") —
   amber, because it is the one fact on this line that is a call to action
   (the Assign-to-me control sits behind the fold this line fronts), not a
   neutral reading. Same amber the app's other notice surfaces use. */
.ticket-header-summary-gap { font-weight: 500; color: var(--amber); white-space: nowrap; }
/* RETIRED 2026-08-10. Round 11 added an interpunct between the collapsed
   line's facts, because "Tom Brady Medium Sales wants a…" read as one clause
   with a floating unlabelled "Medium". That was the right fix for a line of
   bare values — and the labels above are the better one: a fact with a key
   does not need a separator to be told apart from its neighbour, and the
   interpunct would now sit between the props row and the snippet, separating
   two things that are already on different lines. */
/* Round C: the collapsed line's Detail snippet — one quiet truncated run.
   min-width:0 + ellipsis so a long body never wraps the summary to a
   second line; the full text is one click away behind the fold. */
.ticket-header-summary-detail {
  font-weight: 400;
  color: var(--text-quiet);
  min-width: 0;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Selects carry an auto min-width tracking their widest option, and module
   options render as "CODE — Long Name", so without this the value cell has a
   floor wider than its track and the row overflows horizontally on a phone. */
.meta-strip-value { min-width: 0; }
/* Header review §8 round 20 (2026-08-08, sponsor: header "still looks old"):
   the card reads modern but its CONTROLS were the global bordered form
   widgets (app.css `input, select, textarea`) — raw browser dropdowns and a
   dd/mm/yyyy date box are what makes a record header read as a 2005 form.
   Inside the header's VALUE cells they now render as quiet inline values
   (Attio/Linear record-header grammar): borderless and transparent at rest,
   a soft surface appears on hover to say "editable", focus keeps the
   standard accent ring. appearance:none drops the native select chrome; the
   chevron comes back as a neutral #8a93a6 stroke (legible on both themes'
   fills) so the affordance survives. Scoped to .meta-strip-value only —
   the People popover's real form fields and every non-header form keep the
   global recipe. Routes, ids, and the blur/change submit hooks untouched. */
/* §8 round 21: the caps micro-labels themselves — at 11px/0.04em they read
   as legacy table headers. The wireframe's grammar (hx-prop .pk: smaller,
   wider-tracked, lighter) is what makes a properties strip read current;
   scoped to the header so list headers and rail captions keep the shared
   recipe. */
.ticket-header-details .meta-strip-label,
.ticket-header-details .people-group-label {
  font-size: 10px;
  letter-spacing: 0.08em;
  font-weight: 500;
}
/* SPONSOR 2026-08-09 (round 4): "cosmetic on the incident header (also align
   input field with creation — make it discrete)."
   
   The two screens disagreed on the most basic question a control can answer:
   am I a form field or am I a fact? The DETAIL header had already settled it —
   borderless, caps micro-label, a chevron and nothing else, so the card reads
   as a record you can amend rather than a form you must fill. CREATION still
   used bordered form controls, so the same three facts (System/Module,
   Reporter, Priority) arrived in one costume and lived in another.
   
   Creation adopts the header's grammar, not the other way round: the header is
   the screen you see for the rest of the ticket's life, and the create screen
   is the one you see once. Aligning the rare screen to the common one is the
   cheaper direction for a reader to learn.
   
   ONE selector list, both places. The alternative — a second discrete recipe
   scoped to .ticket-header-form — is two things to keep in dark-mode sync
   (PRN-05) for no gain, and the drift this fixes is exactly what that produces.
   The label side rides along the same way (see .ticket-header-form label). */
.ticket-header-details .meta-strip-value select,
.ticket-header-details .meta-strip-value input[type='text'],
.ticket-header-details .meta-strip-value input[type='date'],
.ticket-header-field-control select,
.ticket-header-field-control input[type='text'],
.ticket-header-field-control input[type='date'],
.ticket-header-form .field > select,
.ticket-header-form .field > input[type='text'] {
  border: none;
  background: transparent;
  padding: 2px 6px;
  margin-left: -6px; /* value text aligns with its caps label above */
  border-radius: 6px;
  font-size: 13.5px;
  color: var(--text);
  transition: background-color 0.12s ease-out;
}
.ticket-header-details .meta-strip-value select,
.ticket-header-field-control select,
.ticket-header-form .field > select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 20px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' fill='none' stroke='%238a93a6' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 6px center;
  cursor: pointer;
}
.ticket-header-details .meta-strip-value select:hover,
.ticket-header-details .meta-strip-value input[type='text']:hover,
.ticket-header-details .meta-strip-value input[type='date']:hover,
.ticket-header-field-control select:hover,
.ticket-header-field-control input[type='text']:hover,
.ticket-header-form .field > select:hover,
.ticket-header-form .field > input[type='text']:hover {
  background-color: var(--surface);
}
.ticket-header-details .meta-strip-value select:focus-visible,
.ticket-header-details .meta-strip-value input[type='text']:focus-visible,
.ticket-header-details .meta-strip-value input[type='date']:focus-visible,
.ticket-header-field-control select:focus-visible,
.ticket-header-field-control input[type='text']:focus-visible,
.ticket-header-form .field > select:focus-visible,
.ticket-header-form .field > input[type='text']:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  background-color: var(--surface);
}
.meta-strip-edit select { max-width: 100%; }
/* --pair has been in the markup since the System/Module cascade shipped, with
   no rule anywhere — it passed ui-lint only because the name is grandfathered
   in the baseline. The gap a reader sees between the two selects is the HTML
   whitespace between the tags, not a designed one. Either give it the layout
   its name promises or drop the class; this gives it the layout. */
.meta-strip-edit--pair { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.ticket-qualifier { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
/* Activity, below the composer — its own section now that there is no rail to
   hold it. Same renderer, so the .rail-section rules inside it still apply. */
.ticket-activity-section { display: flex; flex-direction: column; gap: 18px; }
.reading-column { display: flex; flex-direction: column; gap: 28px; }
.reading-column .pref-card { margin-bottom: 0; }
/* Q-64 (sponsor screenshot, 2026-08-14 — "pls check margin between card, pls
   make it standard"): .cr-feed/.reading-column own the 28px rhythm between
   posts, but a repeatable Transport Request post stacks inside its own
   swap-target div (#cr-transport-slot, #incident-transport-slot in
   cr-detail.ts/incident-detail.ts) so htmx can replace the list without
   touching sibling posts. That div is a plain block, not the flex container
   .pref-card's own margin-bottom:0 above is relying on — so two stacked
   Transport Request cards (e.g. a decided one and its Draft) touched with
   0px between them while every other consecutive pair of posts in the same
   feed kept the standard 28px. Same value, same rule, applied to the one
   post type that swaps as a group instead of one at a time. */
#cr-transport-slot,
#incident-transport-slot {
  display: flex;
  flex-direction: column;
  gap: 28px;
}
/* Fast-follow v3 (owner feedback, 2026-07-21 — "can we just show the
   content as medium.com?"): a real article doesn't box each section — it
   flows, separated by whitespace and headings alone. Scoped to the reading
   column ONLY (.pref-card's base boxed recipe stays untouched for every
   other surface that uses it — admin pages, the rail's metadata strip
   elsewhere, etc.) so this doesn't ripple beyond the incident body. */
.reading-column .pref-card {
  background: transparent;
  border: none;
  box-shadow: none;
  border-radius: 0;
}
.reading-column .pref-card-body { padding: 0; }
/* MS-25 (sponsor goal, 2026-08-02: "margin between the align... should be
   consistent between edit and display mode"). Measured: the edit twin's
   rich-text editor (.rt-editor, rich-text-field.ts) carries
   `padding: 10px 12px 44px`; display content had none, so a field's text
   visibly jumped ~12px sideways / ~10px down on every edit<->display
   toggle. This does NOT reintroduce the boxed-card look the 2026-07-21
   "medium.com" rule above deliberately removed (no border, no background)
   — it only aligns where the TEXT starts, so toggling modes moves the
   editor's border into view without moving the words underneath it. */
.pref-card-view-content { padding: 10px 12px 0; }
/* attachmentsRailSection() renders a .rail-section — fine verbatim in the
   narrow rail (CR detail still uses it there), but its 11px uppercase h3
   reads as a compact field label, not a section heading, once it's a peer
   of Description/Root Cause/Solution in the wider reading column. Restyle
   to match those h2s (15px/600) rather than forking the shared component. */
.reading-column .rail-section h3,
/* P4 moved the Activity section OUT of the reading column, into a sibling
   (.ticket-activity-section), so this restyle stopped reaching it and Activity
   alone reverted to an 11px uppercase field label while its peer sections
   render at 15px/600. The rule this selector exists to state — an 11px
   uppercase heading reads as a field label, not a section heading, once it is
   a peer of Description/Root Cause/Solution — did not stop being true when the
   section changed parents. */
.ticket-activity-section .rail-section h3 { font-size: 15px; font-weight: 600; text-transform: none; letter-spacing: normal; color: var(--text); margin: 0 0 8px; }
.root-cause-card h2,
.solution-card h2,
.resolution-confirmation-card h2,
.change-request-widget h2 { font-size: 15px; font-weight: 600; margin: 0; }
/* Command bar tidy-up (2026-07-23): every reading-column section shares
   this heading+attach-icon row (sectionCard(), incident-detail.ts). */
.section-heading-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
/* 10px horizontal stays literal — the spacing steps are 8/12 and nudging a
   button a visible 2px to force a token fit is the "don't force it" case. */
.btn-small { padding: var(--space-2) 10px; font-size: var(--fs-2); }
/* Fast-follow (owner feedback, 2026-07-22 — "check button and x button"):
   Save/Cancel as discrete icon buttons instead of labeled text buttons.
   --green is the app's existing semantic "success" pair (chip-status-
   resolved, .status-active, .proposal-accepted) — reused for Save rather
   than the accent hue, per this app's own "semantic color is separate
   from the accent" convention. aria-label/title carry the text meaning
   for anyone who can't rely on icon shape alone. Sized/positioned as a
   floating pair now (see .rt-floating-actions in the rich-text-field
   block below) rather than a static row under the field. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The label:value metadata pair — no per-field <h3>, no boxed "Create Change
   Request" button (an inline text-link instead).

   The `.meta-strip` CONTAINER that once laid these out as a 2-column grid is
   gone (2026-08-14): nothing has emitted the bare class since the header was
   rebuilt around `.ticket-header-props`, which lays its own children out. The
   two below are still live and still used together, under whichever container
   holds them. */
.meta-strip-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-quiet); }
.meta-strip-value { font-size: 13px; }
.text-link { color: var(--accent); text-decoration: underline; font-size: 13px; }
.text-link:hover { color: var(--accent-on-tint); }

.chat-stream-section .chat-stream { margin-bottom: 8px; }

/* Stage 3 (chat-event filtering): "Show all activity" toggle sits inline
   with the Activity heading rather than adding a second h3-height row. */
.chat-stream-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.chat-stream-heading h3 { margin: 0; }
.chat-activity-toggle { background: none; border: none; padding: 0; cursor: pointer; font: inherit; }

.proposal-byline { font-size: 12px; color: var(--text-quiet); margin: 6px 0 0; }

/* Closing "STATUS: X" line (DEC-032) — a quiet rule + label, distinct from
   (not a duplicate of) the header statusChip badge: badge is glance state,
   this is the read-through moment, owner ruling ships both. */

/* Command bar consolidation (2026-07-23) — the sticky bottom "island":
   comment + a GENERAL attachment drop-zone + status-contextual quick
   actions (Start work/Assign to me/Propose Solution/Accept & Reject/
   Reopen), all previously scattered across the metadata strip, the
   Solution card, and the closing status line. position:sticky (not
   fixed) against .content — the one scroll container on this page (see
   the sticky-sidebar/header work) — so it naturally respects the reading
   column's width and never needs a manual left-offset around the
   sidebar the way position:fixed would. */
/* Layout follow-up (2026-07-23): position:sticky alone only keeps the bar
   inside the viewport WHILE scrolling past its normal flow position — on a
   short incident (content shorter than the viewport) there's nothing to
   scroll past, so the bar just sat wherever it fell in-flow, not glued to
   the bottom of the screen. Standard sticky-footer fix: .incident-detail-
   page becomes a flex column at least as tall as .content's viewport
   (min-height:100%), and the bar's margin-top:auto absorbs any leftover
   space, pushing it to the bottom whenever content is short — sticky then
   takes back over once content actually overflows and auto resolves to 0. */
/* Layout realignment (2026-07-23 follow-up: "pls realign layout incident
   and CR include command bar and right sidebar") — CR's page wrapper needs
   the exact same flex-column/min-height shape for the command bar's
   margin-top:auto sticky-to-bottom trick to work. */
.incident-detail-page, .cr-detail-page { display: flex; flex-direction: column; min-height: 100%; }
/* §8 round 28 (GATE-19 phase 4): the ticket-detail command bar's CSS is
   DEMOLISHED with the component — the sticky glass island, its fit-content
   round-13 sizing, .command-bar-actions*, the inline comment-composer
   recipe and the open-upward .command-bar-more override are all gone
   (every action found its owning surface; the composer owns Comment). The
   home Command Centre's intake form keeps the base .command-bar family
   above — it was always a different component sharing the class name. */
.action-dialog button[disabled] { opacity: 0.5; cursor: not-allowed; }

/* DEC-032 discrete input recipe — a quiet underline that stays visible at
   rest (a11y floor for keyboard users, never fully invisible) and
   intensifies to the accent color on focus. --border already fills the
   "border-quiet" role this recipe calls for (a hairline neutral divider,
   already used the same way at .chat-event's border-bottom) — reused
   rather than introducing a second, unaudited quiet-border token. Applied
   to the Incident Detail rail's (relocated) comment box; CR detail's
   comment box keeps its existing default styling, untouched this pass. */
.comment-box-discrete textarea {
  border: none;
  border-bottom: 1px solid var(--border);
  background: transparent;
  padding: 8px 2px;
  border-radius: 0;
}
.comment-box-discrete textarea:focus {
  outline: none;
  border-bottom-color: var(--accent);
  box-shadow: 0 1px 0 0 var(--accent);
}

/* ---- DEC-031: rich-text-field.ts (contenteditable Detail/Root Cause/
   Solution editor, replacing md-editor-field.ts for incident content) ---- */
/* Fast-follow (owner feedback, 2026-07-22 — "toolbar above ... show when
   highlight the word or statement"): a Medium-style floating/contextual
   toolbar instead of a permanently-visible bar above the editor. It's
   positioned by rich-text-field.ts's script (position: fixed, coordinates
   from the live selection Range) and toggled visible only while there's a
   non-collapsed text selection inside the editor — the .rt-toolbar-visible
   class is the only thing that shows it; base state is invisible AND
   non-interactive (pointer-events: none), not just opacity 0, so a
   stray tab stop can't land on a hidden toolbar. z-index 20 matches this
   app's other floating popovers (.account-menu-panel).  */
.rt-toolbar {
  display: flex;
  gap: 4px;
  flex-wrap: nowrap;
  position: fixed;
  z-index: 20;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-card);
  padding: 4px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.12s ease-out;
}
.rt-toolbar.rt-toolbar-visible { opacity: 1; visibility: visible; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .rt-toolbar { transition: none; }
}
.rt-btn {
  background: transparent;
  border: none;
  border-radius: 6px;
  padding: 4px 8px;
  font-size: 12px;
  color: var(--text);
  cursor: pointer;
}
.rt-btn:hover { background: var(--surface-hover); }
.rt-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.rt-editor {
  min-height: 120px;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  /* Fast-follow v4 (owner feedback, 2026-07-22 — "change textarea to other
     color"): --accent-tint means "this surface is live/editing" — the
     same dual-theme-audited token the AI-prefill sparkle chips and active
     states already use. This is the base/default look, i.e. what an
     always-editing field (Detail, Solution) looks like permanently, and
     what a clickToEdit field (Root Cause) looks like once you're actually
     editing — see .rt-editor--display below for its OTHER state, the
     quiet read-display look, toggled by rich-text-field.ts's script. */
  background: var(--accent-tint);
  color: var(--text);
  font-size: 14px;
  line-height: 1.6;
  transition: background-color 0.12s ease-out, box-shadow 0.12s ease-out;
}
.rt-editor:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-tint);
}
/* PHASE 1 (docs/TICKET-PAGE-REDESIGN.md follow-up, 2026-08-07 — "richText
   should be configurable and set to fit content of communication type"):
   two size tiers on top of the 120px base above. Modifier classes, not
   inline pixel values — same discipline every other sizing decision in this
   file follows, and what lets ui-lint register/track them instead of a
   literal appearing wherever a caller happens to want one. */
.rt-editor--compact { min-height: 64px; }
.rt-editor--tall { min-height: 200px; }
.rt-editor:empty::before { content: attr(data-placeholder); color: var(--text-quiet); }
.rt-editor p { margin: 0 0 10px; }
.rt-editor p:last-child { margin-bottom: 0; }
.rt-editor ul { margin: 0 0 10px; padding-left: 20px; }
@media (prefers-reduced-motion: reduce) {
  .rt-editor { transition: none; }
}
/* WYSIWYG click-to-edit (owner feedback, 2026-07-22): the read-display
   state — SAME element as the editor above, contenteditable="false",
   quiet card look instead of the "live" accent tint. cursor + hover/focus
   ring signal it's clickable; the ring uses --accent so it reads as "this
   turns into the same accent-tinted editor", not just a generic hover.
   --surface (not --surface-hover, which is a bare alias for --gray-50 —
   the SAME value as body's own --bg in light theme and would be
   invisible against the page) is guaranteed distinct from --bg in both
   theme bindings. */
/* ROUND 9'S TREATMENT IS GONE (found 2026-08-14 by a cascade sweep, deleted
   here). It set `background: var(--surface)`, `border-color` and
   `cursor: pointer` on this exact selector, plus inset box-shadows on :hover
   and :focus-visible — and round 11 below, same selectors and later in the
   file, sets `border: none`, `background: transparent`, `cursor: text` and
   `box-shadow: none` on all three. Every declaration round 9 made was
   overridden by round 11 for every element that could ever match it, so the
   block rendered nothing at all. The reasoning above it is kept because it
   explains why the surface was chosen; the rules are not, because a rule that
   cannot be seen is a rule the next reader will try to reason about. */
/* Header review §8 round 9 (2026-08-08): inside the HEADER's detail slot the
   rest state is the record's BODY, not a form control — round 7 already
   stripped the wrapper (.ticket-header-detail-slot .ticket-detail-field),
   but the inner .rt-editor--display kept its own border, surface and the
   editor's min-height, so one line of Detail sat in a ~200px box. At rest
   it now renders as plain document text at content height; hover/focus show
   the wireframe's dashed affordance (outline, not border, so nothing
   shifts). Clicking still swaps to the full accent-tinted editor — the
   editing state is untouched, and feed cards (Root Cause/Solution etc.)
   keep their boxed look: this is scoped to the header slot alone. */
/* Round 11 (sponsor 2026-08-08: "for richtext area, can it be inline edit
   and can it be WYSIWYG (discretely)?"): YES to both, everywhere — not just
   in the header. At REST a rich-text field is now plain document text: no
   box, no field chrome, nothing announcing "form". Hovering shows a dashed
   outline (outline, not border, so nothing reflows), and clicking turns the
   SAME element into the accent-tinted WYSIWYG editor it already was. The
   discretion is the point: you read a record, and the places you may write
   reveal themselves under the cursor instead of shouting at rest. Scoped to
   click-to-edit fields — an always-editing field (a composer textarea) still
   looks like what it is. */
.rt-editor.rt-editor--display {
  border: none;
  background: transparent;
  border-radius: 0;
  padding: 0;
  min-height: 0;
  cursor: text;
}
.rt-editor.rt-editor--display:hover {
  box-shadow: none;
  outline: 1px dashed var(--border);
  outline-offset: 6px;
}
.rt-editor.rt-editor--display:focus-visible {
  box-shadow: none;
  outline: 2px dashed var(--accent);
  outline-offset: 6px;
}
/* SPONSOR 2026-08-10: "Rich text — WYSIWYG (discretely) .. just change
   background when on edit mode ... but it should be WYSIWYG, edit & display
   must be same."
   THE GAP THIS CLOSES. Round 11 made the REST state plain document text, which
   was the sponsor's own earlier ask and worked. The EDIT state was left as it
   had always been: a 120px-min bordered box with its own padding. So clicking
   into a one-line Detail inflated it into a form control four times its height
   — the text jumped, the layout reflowed, and for a moment you were not sure
   you were still looking at the same paragraph. That is the opposite of
   WYSIWYG: what you see and what you edit were two different objects.
   Now, for CLICK-TO-EDIT fields only, editing keeps the document's geometry —
   same box model, same type, same position — and changes only the COLOUR.
   Scoped by [data-rt-click-to-edit] so an always-editing field (a composer
   textarea) still looks like the form control it honestly is.
   min-height survives at ~2 lines: at content height an EMPTY field collapses
   to a caret with no target to click back into, which is a different bug. */
/* SPONSOR 2026-08-10: "button to popup for full editor tool (discrete expand
   icon on top right of the rich editor area)".
   DISCRETE means it costs nothing when unused: absent until the field is
   hovered or focused, and positioned OVER the text's top-right rather than
   above it, so it never displaces a line and the "edit and display are the
   same shape" rule survives it. The wrap is the positioning context. */
.rt-field-editor-wrap { position: relative; }
.rt-expand {
  position: absolute;
  top: -2px;
  right: -2px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-quiet);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s ease-out;
}
/* Focus-within, not just hover: a keyboard user must be able to reach it, and
   :focus-visible on the button itself is too late — it is opacity 0 until
   something in the field has focus. */
.rt-field-editor-wrap:hover .rt-expand,
.rt-field-editor-wrap:focus-within .rt-expand { opacity: 1; }
.rt-expand:hover { color: var(--accent); border-color: var(--accent); }
.rt-expand:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) { .rt-expand { transition: none; } }
/* The popup. This is the ONE place a permanent toolbar is right: it exists
   for the case where hunting a selection bubble is the wrong interaction. */
.rt-expand-dialog {
  /* SPONSOR 2026-08-10: "too small? can we cover 80% of screen?" — yes. This
     is the surface you open BECAUSE the inline field is not enough room, so
     760px was answering a question nobody asked. 80% of both axes, capped at
     92vw/92vh so it never touches the viewport edge and the backdrop stays
     visible as a way out. */
  width: 80vw;
  max-width: 92vw;
  height: 80vh;
  max-height: 92vh;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  padding: 0;
  box-shadow: var(--shadow-card);
}
/* display ONLY when open. A <dialog> is display:none until it is opened, and
   setting `display` unconditionally overrides that — the dialog then renders
   ALWAYS, in normal document flow at its position in the markup, with no
   backdrop and nothing seeded into it. Which is exactly what the sponsor saw:
   an empty editor wedged into the page under the Detail field.
   The flex column is what lets the editor grow into the 80vh, so it is needed
   — it just has to be scoped to the state where the dialog is meant to exist.
   My own check missed it by only ever asserting the OPEN state. */
.rt-expand-dialog[open] { display: flex; flex-direction: column; }
.rt-expand-dialog::backdrop { background: rgb(16 24 40 / 45%); }
.rt-expand-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}
.rt-expand-title { font-size: 13px; font-weight: 600; }
.rt-expand-toolbar {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--border);
}
/* Inside the dialog the editor IS the form control, so it keeps the boxed
   editor look the inline field gave up — nothing here is pretending to be a
   document. */
/* The editor takes ALL the room the dialog just gained — a taller dialog with
   a short editor floating in it would have moved the problem rather than
   solved it. flex:1 with min-height:0 so it can shrink inside the flex column
   instead of overflowing it. */
.rt-expand-editor {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin: var(--space-5);
  width: calc(100% - var(--space-5) * 2);
}
/* MS-30 §15 A1 — an attachment referenced inside a body. Only ever emitted by
   markdown.ts after the reference resolved to a file on THIS ticket, so these
   two recipes can never be pointed at an external host. */
.md-image {
  display: block;
  max-width: min(420px, 100%);
  height: auto;
  margin: var(--space-4) 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
/* The NON-image case. A chip, not a bare link: it has to read as "a file lives
   here" at a glance, which an underlined filename in a sentence does not. */
.md-file {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--chip-bg);
  color: var(--chip-text);
}
.md-file::before { content: "\1F4C4"; }
.md-file:hover { border-color: var(--accent); color: var(--accent); }
[data-rt-click-to-edit] .rt-editor:not(.rt-editor--display) {
  border: none;
  border-radius: 4px;
  padding: 0;
  min-height: 2.9em;
  /* The one thing that changes. A wash, not a border — a border would put the
     box back and undo the whole point. */
  background: var(--accent-tint);
  /* Bled outward so the tint frames the text instead of starting hard against
     the first character, without the padding shifting a single glyph. */
  box-shadow: 0 0 0 6px var(--accent-tint);
}
[data-rt-click-to-edit] .rt-editor:not(.rt-editor--display):focus {
  outline: none;
  box-shadow: 0 0 0 6px var(--accent-tint), 0 0 0 7px var(--accent);
}
.ticket-header-detail-slot .rt-editor.rt-editor--display {
  border: none;
  background: transparent;
  border-radius: 0;
  padding: 0;
  min-height: 0;
  cursor: text;
}
.ticket-header-detail-slot .rt-editor.rt-editor--display:hover {
  box-shadow: none;
  outline: 1px dashed var(--border);
  outline-offset: 6px;
}
.ticket-header-detail-slot .rt-editor.rt-editor--display:focus-visible {
  box-shadow: none;
  outline: 2px dashed var(--accent);
  outline-offset: 6px;
}
/* The one region a click-to-edit field's floating Save/Cancel anchors to —
   position:relative wraps just the editor (not the toolbar/label above),
   so "bottom-right corner" means the editor's own corner. */
.rt-field-editor-wrap { position: relative; }
/* NOT FLOATING ANY MORE (sponsor screenshot, 2026-08-12: "why it is floating?
   unsave / discard / save"). This was position:absolute in the editor's
   bottom-right corner, which worked while it held two 30px icons — it tucked
   into the corner and covered nothing anyone was reading. Standardising the
   buttons to the words Discard and Save (the sponsor's own earlier ask) took
   it from ~110px to ~300px, and an absolutely-positioned 300px strip sat
   squarely across the second line of the text being edited.
   A row BELOW the editor instead — where the Detail card's identical row has
   always been, and the card that was held up as the pattern to copy. Nothing
   can overlap text it is not positioned over. `display:none` rather than
   visibility:hidden now that it occupies real space: a reserved empty strip
   under every clean field would be its own kind of noise. */
.rt-floating-actions {
  display: none;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  margin-top: var(--space-3);
}
.rt-field-editor-wrap.rt-dirty .rt-floating-actions { display: flex; }
@media (prefers-reduced-motion: reduce) {
  .rt-floating-actions { transition: none; }
}
/* Owner feedback (2026-07-22 — "show a small note ... unsaved"): a quiet
   pill next to the floating Save/Cancel, visible under the exact same
   .rt-dirty condition as the buttons themselves. --amber/--amber-tint is
   this app's existing "notice" semantic pair (already used for the proxy
   banner) — reused here rather than plain gray, since "you have unsaved
   changes" is a real notice, not decoration, and semantic color is kept
   separate from the accent hue per this app's own convention. */
.rt-unsaved-note {
  font-size: 11px;
  font-weight: 600;
  color: var(--amber);
  background: var(--amber-tint);
  border-radius: 999px;
  padding: 4px 10px;
  white-space: nowrap;
  box-shadow: var(--shadow-card);
}
/* Disabled-until-filled hint (2026-07-23, submitDisabled/disabledHint) —
   e.g. Propose Solution's Save button while Root Cause is still blank.
   Same pill treatment as --rt-unsaved-note above, --red semantics since
   this is "you can't submit yet" rather than a neutral in-progress note. */
.rt-disabled-hint {
  font-size: 11px;
  font-weight: 600;
  color: var(--red);
  background: var(--red-tint);
  border-radius: 999px;
  padding: 4px 10px;
  white-space: nowrap;
  box-shadow: var(--shadow-card);
}

/* 2026-07-24 follow-up ("or button Save/Cancel just like before", then "can
   we have fold button on right-bottom corner of textarea instead? for
   save/cancel" — owner declined auto-save; wants the existing explicit Save
   button left exactly as-is, plus a visible unsaved-changes indicator and a
   Cancel, on EVERY rich-text field, not just clickToEdit ones, tucked into
   the editor's own bottom-right corner). This is the SAME corner treatment
   as .rt-floating-actions above — same absolute position, same reserved
   padding, same icon-button Cancel — for fields that don't have their own
   floating Save (grouped/command-bar-committed fields have no per-field
   submit target, so no Save icon here; Cancel-only). Rendered as a sibling
   INSIDE .rt-field-editor-wrap (mutually exclusive with .rt-floating-actions
   — a field never gets both), so it inherits the same position:relative
   anchor. */
.rt-editor { padding-bottom: 44px; }
/* The same unfloating as .rt-floating-actions above, and for the same reason:
   the two rows are the two halves of one treatment, and fixing the overlap on
   only the half the sponsor happened to screenshot would leave the other to be
   reported next week. */
.rt-inline-status {
  display: none;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  margin-top: var(--space-3);
}
.rt-field-editor-wrap.rt-dirty .rt-inline-status { display: flex; }

/* ---- CR stepper / gates ---- */
/* Compact phone summary — hidden on desktop, replaces the circles at <=768px
   where nine labels cannot fit (they overlapped into unreadable text). */
.stepper-compact { display: none; }
.stepper-compact-line { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.stepper-compact-step { font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-quiet); }
.stepper-compact-label { font-size: 14px; font-weight: 600; color: var(--text); }
.stepper-compact-track { height: 4px; border-radius: 999px; background: var(--border); overflow: hidden; }
.stepper-compact-fill { height: 100%; border-radius: 999px; background: var(--accent); }
/* The circles used to be display:none here, because nine labels squeezed into
   a 390px row overlapped into unreadable mush. They come back at <=768px as a
   HORIZONTALLY SCROLLING strip (2026-07-29: the steps are click-to-navigate
   controls now, and a control that only exists on desktop is not a control) —
   each step keeps a fixed width instead of dividing the viewport, so the
   labels have the same room they have on a wide screen and the row scrolls
   rather than crushing. The compact summary line stays above them: it answers
   "where am I" without scrolling. */
/* CR workflow redesign (owner feedback, 2026-07-23: "progress bars -
   circles (pls check out tailwind as sample)") — the Tailwind UI
   "Progress bar — Circles" pattern: numbered circles joined by a
   connecting line that fills in as each stage completes, a checkmark once
   done, a ring on the active step. */
/* Bottom margin trimmed to 4px (was 22px). The .ticket-sticky-header anchor is
   a permanently-reserved, empty 32px sticky slot that sits in flow right after
   this, so 22px + 32px opened ~54px of dead space between the stepper and the
   header block — invisible before P4, when a reading grid followed rather than
   a tight label/value block. The anchor itself must stay where it is and keep
   margin-top:0 (see .ticket-sticky-header: both a DOM move and a negative
   margin desync it from the content it overlays), so the compensation belongs
   here. .stepper-compact keeps its own spacing for the phone presentation. */
.stepper-circles { display: flex; list-style: none; padding: 0; margin: 0 0 4px; }
.stepper-circle-item { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; text-align: center; min-width: 0; }
.stepper-circle-item:not(:first-child)::before {
  content: '';
  position: absolute;
  top: 13px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: var(--gray-100);
  z-index: 0;
}
.stepper-circle-item.done:not(:first-child)::before,
.stepper-circle-item.active:not(:first-child)::before {
  background: var(--accent);
}
.stepper-circle {
  position: relative;
  z-index: 1;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  background: var(--gray-100);
  color: var(--text-quiet);
  border: 2px solid transparent;
}
.stepper-circle-item.done .stepper-circle { background: var(--accent); color: var(--on-accent); }
.stepper-circle-item.active .stepper-circle { background: var(--surface); color: var(--accent); border-color: var(--accent); }
.stepper-circle-label { margin-top: 6px; font-size: 10.5px; color: var(--text-quiet); max-width: 92px; line-height: 1.3; }
/* The active phase names its current status underneath, so grouping nine
   statuses into six phase circles loses no precision — it just stops shouting
   the detail at someone who only wants "where am I". Rendered only on the
   ACTIVE node (see stepper()): on a completed phase it would be stale, and on
   an upcoming one it would be a guess. Deliberately quieter and smaller than
   the phase label it sits under. */
/* MOVED here from above the base rules (2026-08-06). Media queries add no
   specificity, so sitting earlier in the file meant every declaration in this
   block LOST to its base-rule counterpart: `flex: 0 0 86px` lost to `flex: 1`,
   and `margin-bottom` lost to the `margin` SHORTHAND. The phone presentation
   this block exists to create — fixed-width steps that scroll instead of
   dividing the viewport — has therefore never actually rendered. `margin` is
   now the shorthand here too, so it cannot be half-overridden again. */
@media (max-width: 768px) {
  .stepper-compact { display: block; margin-bottom: 10px; }
  .stepper-circles { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 0 0 18px; padding-bottom: 4px; }
  .stepper-circle-item { flex: 0 0 86px; }
}

.stepper-circle-sub {
  display: block;
  margin-top: 2px;
  font-size: 9.5px;
  line-height: 1.25;
  color: var(--text-quiet);
  opacity: 0.85;
  max-width: 92px;
}
.stepper-circle-item.active .stepper-circle-label { color: var(--text); font-weight: 600; }
.stepper { margin-bottom: 20px; }
.step-rejected { display: inline-block; background: var(--red-tint); color: var(--red); padding: 6px 10px; border-radius: 999px; font-size: 12px; }

/* CR workflow redesign: Plan lock banner, Deployment submission rows —
   reuse existing tokens (approval-banner amber, pref-card/chip families),
   no new color system. */
/* The transport list, numbered. The ORDER IS THE IMPORT ORDER — SAP runs them
   in sequence — so it is shown as an ordered list rather than a comma-joined
   line, which gave a reader no way to tell whether the sequence they typed had
   survived. Monospace because a transport id is an identifier, not prose. */
.tp-sequence {
  margin: 0 0 6px;
  padding-left: 22px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  color: var(--text);
}
.tp-sequence li { margin: 1px 0; }

/* `.deployment-submission-row` lived here until the Transport Request card
   became the shared communication card (2026-08-13): its box recipe was kept
   only because the retired amend disclosure's `:has()` rules hung off it, and
   both went together. Deleted rather than left as a rule nothing emits. */
/* `.deployment-submission-header` lived here until MS-30 P3 turned each
   DeploymentSubmission into a feed post: the header row it styled is now
   `.feed-post-head`, and nothing emitted the class any more. Deleted rather
   than left as a rule nobody can reach. */
/* WAS a second `.field-row` definition, colliding with the form-field row at
   the top of this file (UX round 1, 2026-07-30). Two components, one class
   name — the same disease as `.command-bar`. The collision was not cosmetic:
   `.field-row label { flex: 0 0 110px }` sets a flex BASIS on the main axis, and
   the create form's `.field` is a COLUMN, so 110px landed on the label's HEIGHT.
   Every "System"/"Module" label on the new-ticket form was a 110px-tall box
   holding one line of text, leaving ~90px of dead space in the middle of the
   most-used form in the app.
   Renamed to what it actually is: the invite widget's link+button row. Its
   label rule is gone entirely — that row has no label and never had one, so the
   rule's only remaining effect was the bug. */
.invite-link-row { display: flex; gap: 8px; margin-bottom: 6px; align-items: center; }
.field-list { margin-bottom: 10px; }

/* Sponsor Q (2026-08-02, "why still color area around action button?" and
   the follow-up "approve/reject are still have box area?"): the notice-box
   chrome (background/border/padding/radius) used to apply unconditionally,
   including to a live, actionable decision (Approve/Reject) with no
   message inside it at all — UAT_SIGNOFF and Incident's
   decisionCard(gate: null) callers both suppress `waitingOn`
   (copy.waitingOn = ''), since a "Waiting on X" sentence is already shown
   elsewhere on those pages. First fix only dropped the amber color, but a
   neutral bordered/padded box is STILL a box — a card drawn around
   Approve/Reject that Comment, sitting right beside it in the same bar,
   never gets. All box chrome now lives ONLY on `.approval-banner-note`,
   added by decisionCard() precisely when it has a message to show. The
   base `.approval-banner` is unstyled — when there is nothing to say, the
   buttons sit flush in the command bar like every other action. The
   read-only "waiting, not your turn" state (cr-detail.ts's various inline
   banners, always carry a message) keeps the `-note` modifier and its
   card treatment unchanged. */
.approval-banner-note {
  background: var(--amber-tint);
  border: 1px solid var(--amber-border);
  color: var(--amber);
  padding: 14px 18px;
  border-radius: 8px;
  margin-bottom: 20px;
}
.approval-banner p { margin: 0 0 8px; font-weight: 600; }
.decision-card { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* The note field is on the SAME SCALE as the pair beside it (2026-08-13). The
   buttons went small when every decision moved inside a card; the input did
   not, so the row ran 38px field against 27px buttons — the misalignment the
   sponsor was pointing at ("pls align layout & size") survived the button fix
   by itself. Same padding and font as `.btn-small`, so the three controls agree
   on their height rather than on their vertical centre. */
.decision-card input { flex: 1; min-width: 180px; padding: 4px 10px; font-size: 12px; }


/* ---- Attachments (THE-35) ---- */
.attachment-size { color: var(--text-quiet); font-size: 11px; }
.attach-optimize-note { font-size: 11px; color: var(--text-quiet); margin: 4px 0; }
.attach-optimize-note label { font-weight: 400; margin-left: 6px; }

/* ---- Dropzone.js attachment drop-zone (2026-07-23 swap) ----
   Dropzone owns the DOM inside .attach-dropzone once initialized
   (dz-message / dz-preview / dz-progress etc.) — styled here from this
   app's own tokens rather than importing Dropzone's bundled basic.css,
   which would clash with the design system. */
.attach-dropzone {
  margin-top: 10px;
  min-height: 110px;
  border: 1.5px dashed var(--border);
  border-radius: var(--radius-lg);
  /* §8 round 29 (sponsor: "dropzone — better?"): same quiet-wash recipe as
     the round-18 header card (color-mix of --text into --bg) so the zone
     reads as a recessed TARGET, not another surface card; --surface stays
     the pre-color-mix fallback. */
  background: var(--surface);
  background: color-mix(in srgb, var(--text) 2%, var(--bg));
  padding: 16px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  transition: border-color 0.15s ease-out, background 0.15s ease-out;
}
.attach-dropzone.dz-drag-hover { border-color: var(--accent); background: var(--accent-tint); }
.attach-dropzone .dz-message { color: var(--text-quiet); font-size: 13px; text-align: center; margin: 0; }
/* §8 round 29: Dropzone wraps its default message in a real <button>
   (.dz-button), which inherited the app's bordered button chrome — the
   copy rendered as a small outlined button floating in the zone, which is
   what made the dialog read as unstyled. The whole ZONE is the click
   target; the inner button is presentation only. */
.attach-dropzone .dz-button {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--text-quiet);
  cursor: pointer;
}
/* The quiet second line under the copy — states the auto-optimize behavior
   the init script applies (canvas-downscale of oversized images) instead of
   leaving it a surprise. Injected via CSS content on the message, not the
   template, so dropzone-init.js's message config stays the single source of
   the primary copy. */
.attach-dropzone .dz-message::after {
  content: "Images over 500 KB are optimized automatically";
  display: block;
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-quiet);
  opacity: 0.75;
}
.attach-dropzone .dz-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}
.attach-dropzone .dz-preview:last-child { border-bottom: none; }
.attach-dropzone .dz-filename { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-dropzone .dz-size { color: var(--text-quiet); font-size: 11px; flex: none; }
.attach-dropzone .dz-progress {
  width: 60px;
  height: 4px;
  border-radius: 999px;
  background: var(--gray-100);
  overflow: hidden;
  flex: none;
}
.attach-dropzone .dz-upload { display: block; height: 100%; background: var(--accent); transition: width 0.2s ease-out; }
.attach-dropzone .dz-success-mark { color: var(--green); flex: none; }
.attach-dropzone .dz-error-mark { color: var(--red); flex: none; }
.attach-dropzone .dz-error-message { color: var(--red); font-size: 11px; flex: none; }
.attach-dropzone .dz-remove { color: var(--text-quiet); font-size: 11px; flex: none; cursor: pointer; }
.attach-dropzone .dz-remove:hover { color: var(--red); text-decoration: none; }

/* ---- Projects board (THE-36) ---- */
.board-lanes { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; align-items: start; }
/* D2: stack lanes vertically on mobile — each .board-lane already
   self-contains its own scrollable card list, so no horizontal-scroll
   affordance is needed. */
@media (max-width: 768px) {
  .board-lanes { grid-template-columns: 1fr; gap: 20px; }
}
/* MS-15 Round 43: background was the untthemed var(--gray-50) literal — no
   dark binding, so every lane (not just the empty ones, just most visible
   there since cards cover the rest) rendered a near-white #f7f8fa panel on
   the dark page. Same B2-class bug (see .admin-member-row above), missed
   here — --surface-hover carries the dark override this needs. */
.board-lane { background: var(--surface-hover); border-radius: 10px; padding: 12px; min-height: 120px; }
.board-lane-header { font-size: 12px; font-weight: 600; text-transform: uppercase; color: var(--text-quiet); display: flex; justify-content: space-between; margin-bottom: 10px; }
.board-lane-count { background: var(--gray-100); border-radius: 999px; padding: 1px 8px; font-size: 11px; }
.board-lane-cards { display: flex; flex-direction: column; gap: 10px; }
.board-lane-empty { color: var(--text-quiet); font-size: 12px; text-align: center; }
.empty-state-lane { margin-top: 24px; }

.project-card { display: block; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px; color: var(--text); }
.project-card:hover { text-decoration: none; border-color: var(--accent); }
.project-card-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 6px; }
.project-card-top h4 { margin: 0; font-size: 13px; }
.project-card-meta { display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
.project-card-footer { font-size: 11px; color: var(--text-quiet); }
.project-card-linked { font-size: 11px; color: var(--text-quiet); }

.avatar-initials { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 999px; background: var(--accent-tint); color: var(--accent-on-tint); font-size: 11px; font-weight: 700; }

.card-menu { position: relative; }
.card-menu summary:not(.btn) { list-style: none; cursor: pointer; color: var(--text-quiet); padding: 0 4px; }
.card-menu summary.btn { list-style: none; }
.card-menu summary::-webkit-details-marker { display: none; }
.card-menu-list { position: absolute; right: 0; z-index: 5; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); min-width: 180px; padding: var(--space-2); }
.card-menu-list form { margin: 0; }
/* padding stays 8px/10px, not var(--space-4)/var(--space-5): 10px doesn't
   cleanly match a surveyed value, left literal rather than forced. */
.card-menu-item { display: block; width: 100%; text-align: left; background: none; border: none; padding: var(--space-4) 10px; font-size: 12px; border-radius: var(--radius-sm); color: var(--text); }
.card-menu-item:hover { background: var(--surface-hover); cursor: pointer; }
/* Delete is irreversible (removes the row + history — Cancel is just a
   status change): red is earned on hover, not worn at rest, so the item
   doesn't out-shout Cancel sitting next to it in the same menu. ONE rule
   for the four independently-built destructive controls that already
   followed this exact grammar (L2 standardisation, 2026-08-23) — each
   still lives on its own differently-shaped host (.card-menu-item text
   row, .icon-btn square, a .btn.btn-quiet.btn-small labeled button, a
   bare unstyled button), so this merges only the shared hover-color
   declaration, not the components themselves. .attachment-delete-btn
   keeps its own extra hover background below — the others never had one,
   and giving them one now would be a real visual change, not a dedup. */
.card-menu-item-danger:hover,
.attachment-delete-btn:hover,
.invite-delete-btn:hover,
.saved-view-delete:hover { color: var(--red); }

/* Per-section attachment trigger (2026-07-23, then follow-up): a compact
   icon button that opens a .action-dialog (same modal grammar as Comment/
   Reopen) holding just the drop-zone — the attachment LIST itself now
   lives inline under the section (see .attachments-list-discrete below),
   not inside this trigger's popover any more. */
.attach-count {
  font-size: 10px;
  font-weight: 700;
  background: var(--accent-tint);
  color: var(--accent-on-tint);
  border-radius: 999px;
  padding: 1px 6px;
  line-height: 1.4;
  margin-left: 4px;
}

/* Discrete attachments list (2026-07-23 follow-up: "show as a list below
   attached section... show discretely") — small/quiet type, not a card
   grid; each row's description is click-to-edit-in-place (a plain input,
   transparent until hover/focus) rather than a separate save step. */
.attachments-list-discrete { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
.attachment-row-discrete { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-quiet); }
.attachment-row-discrete a { color: var(--text-quiet); }
.attachment-row-discrete a:hover { color: var(--accent); }
.attachment-row-discrete .attachment-size { flex: none; white-space: nowrap; }
.attachment-description-form { flex: 1; margin: 0; }
.attachment-description-form input[type="text"] {
  width: 100%;
  font-size: 12px;
  color: var(--text-quiet);
  border: 1px solid transparent;
  background: transparent;
  padding: 2px 6px;
  border-radius: 4px;
}
.attachment-description-form input[type="text"]:hover { border-color: var(--border); }
.attachment-description-form input[type="text"]:focus { border-color: var(--accent); background: var(--surface); outline: none; }
/* 2026-07-23 follow-up: "if section is not editable, do not allow to
   change the description" — a non-editable section's existing
   description renders as plain text, not an input, in the same slot. */
.attachment-description-readonly { flex: 1; padding: 2px 6px; }
/* 2026-07-23 follow-up: "if section is editable, allow to delete the
   attachment" — a quiet, small icon-button, only ever rendered (by the
   server) while the section is editable. */
.attachment-delete-btn { flex: none; width: 22px; height: 22px; padding: 0; color: var(--text-quiet); }
.attachment-delete-btn:hover { background: var(--surface-hover); }

/* 2026-07-23 follow-up: attachment preview + separate download button.
   The filename itself is now a plain <button> (not an <a>) since it
   opens a dialog rather than navigating — styled to still read as a
   link, the existing affordance users already know. */
.attachment-preview-trigger {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--text-quiet);
  text-decoration: underline;
  cursor: pointer;
}
.attachment-preview-trigger:hover { color: var(--accent); }
.attachment-download-btn { flex: none; width: 22px; height: 22px; padding: 0; color: var(--text-quiet); }
.attachment-download-btn:hover { color: var(--accent); background: var(--surface-hover); }
.attachment-preview-body { min-height: 120px; }
.attachment-preview-image { max-width: 100%; max-height: 60vh; display: block; margin: 0 auto; border-radius: 6px; }
/* 2026-07-23 follow-up: PDF preview — the browser's own built-in viewer,
   just needs room to be useful. */
.attachment-preview-pdf { width: 100%; height: 60vh; border: 1px solid var(--border); border-radius: 6px; }
/* 2026-07-23 follow-up: .eml preview — header fields read like a real
   mail client's summary block, quiet and compact above the body. */
.attachment-preview-email-header { border-bottom: 1px solid var(--border); padding-bottom: 10px; margin-bottom: 10px; }
.attachment-preview-email-header p { margin: 2px 0; font-size: 13px; }
.attachment-preview-email-body img { max-width: 100%; }
.attachment-preview-text {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 13px;
  background: var(--surface-hover);
  border-radius: 8px;
  padding: 12px;
  margin: 0;
}
.attachment-preview-docx table, .attachment-preview-docx th, .attachment-preview-docx td { border: 1px solid var(--border); border-collapse: collapse; padding: 6px 8px; }

/* Change Request widget (round-table decision, built 2026-07-23 follow-up):
   after Resolution Confirmation — a live-status list of any linked CRs
   plus a click-to-reveal inline create form (native <details>, same
   zero-JS disclosure as .card-menu). */
.change-request-widget .linked-cr-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.linked-cr-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; color: var(--text); }
.linked-cr-row:hover { border-color: var(--accent); background: var(--surface-hover); }
.linked-cr-case { font-weight: 600; font-size: 12px; color: var(--text-quiet); flex: none; }
.linked-cr-subject { flex: 1; font-size: 13px; }
.linked-cr-create-form { display: flex; gap: 8px; margin-top: 8px; }
.linked-cr-create-form input[type="text"] { flex: 1; }

/* The display/edit mode toggle switch (2026-07-23) lived here until
   2026-08-07 — header review §8 round B/D retired the toggle (see
   ticket-detail-shared.ts's renderModeToggle tombstone); the recipe was
   deleted with its markup. */

.link-ticket-form { display: flex; gap: 8px; align-items: center; }

/* ---- Board room (THE-36, thin) ---- */
.board-group { margin: 20px 0; }
.board-group h3 { font-size: 12px; text-transform: uppercase; color: var(--text-quiet); display: flex; align-items: center; gap: 8px; }
.board-row { gap: 10px; }
.board-row-updated { font-size: 11px; color: var(--text-quiet); width: 80px; text-align: right; }
/* Fix 10 (ux-cycle1): board-room person chips — no explicit color, so the
   name inherited --text (near-white in dark) on a background still pinned
   to light-mode --gray-100, i.e. white-on-white ("who disappears"). */
.waiting-on { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: var(--chip-bg); color: var(--chip-text); }
.waiting-on-amber { background: var(--amber-tint); color: var(--amber); }
.waiting-on-red { background: var(--red-tint); color: var(--red); }
.board-list { display: flex; flex-direction: column; gap: 4px; margin-top: 16px; }

/* ---- Linear-preferences grammar (panel-ratified 2026-07-20 — a
   REFINEMENT of the Cloudflare-calm north star, same family). Applied this
   slice to Platform Settings + the admin forms named in that ruling; not
   yet rolled out app-wide. .pref-card carries the elevation, zero padding
   of its own — content sits in a padded wrapper or in .pref-row children.
   .pref-row is the "label+description left, control right" line, with a
   hairline border between siblings (not on the first). ---- */
.pref-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: 0; }
.pref-card-body { padding: var(--space-6); }
/* Fix 4 (ux-cycle1): pinned Description block on ticket detail pages —
   reuses .pref-card, adds only the wrapping/margin the free-text detail
   field needs (the reporter's words, unlike the other one-line rail facts). */
.ticket-description-text { white-space: pre-wrap; color: var(--text); margin: 8px 0 0; }
.pref-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 44px; padding: 12px 16px; }
.pref-row + .pref-row { border-top: 1px solid var(--border); }
.pref-row-text { display: flex; flex-direction: column; gap: 2px; }
.pref-row-label { font-size: 13px; font-weight: 500; color: var(--text); }
.pref-row-desc { font-size: 12px; color: var(--text-soft); }
.pref-section-title { font-size: 15px; font-weight: 600; color: var(--text); margin: 0 0 8px; }
.pref-section-title.eyebrow { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-soft); }

/* ---- Admin gear entry (DEC-011, THE-42) ---- */
.sidebar-footer { margin-top: auto; padding-top: 8px; border-top: 1px solid var(--sidebar-divider); }
.sidebar-gear { font-size: 13px; }

/* ---- Dedicated admin sidebar (DEC-011 Amendment A1) — reuses .sidebar's
   tokens verbatim (--sidebar-bg/--sidebar-text/--accent): no new colors, no
   accent shift, no admin theme. Just a wordmark row + back link recipe.
   Deliberately designed for BOTH themes (design-polish rulings bullet 2):
   the shield glyph + "ADMINISTRATION" + Back-to-app link all resolve
   through --sidebar-text/--sidebar-text-quiet, so the light sidebar's
   platform zone stays legible and visually distinct without new colors. ---- */
.sidebar-admin-back { margin: 0 0 4px; font-weight: 600; }
.sidebar-admin-wordmark {
  display: flex;
  align-items: center;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--sidebar-text-quiet);
  padding: 4px 10px 12px;
  border-bottom: 1px solid var(--sidebar-divider);
  margin-bottom: 10px;
}
.sidebar-admin-hint { padding: 8px 10px 0; }

/* ---- Breadcrumb (Wave 3, linear-refinement, 2026-07-20) — promoted from
   THE-42's .admin-breadcrumb into one shared recipe both shells render onto
   (renderBreadcrumb, web/views/layout.ts). Quiet 12px line, last crumb
   strong/current (never a link); ancestors are plain links. Admin's own
   inline breadcrumb (renderAdminBreadcrumb) renders the identical class,
   inline in its page body (untouched by the move below). ---- */
.breadcrumb { color: var(--text-quiet); font-size: 12px; margin: 0 0 12px; }
.breadcrumb strong { color: var(--text); font-weight: 600; }
.breadcrumb a { color: var(--text-quiet); }
.breadcrumb a:hover { color: var(--text); }
.breadcrumb-sep { color: var(--text-quiet); }
/* Owner feedback (2026-07-22): the app-page breadcrumb (renderHeader) moved
   out of .content and into the topbar, next to brand, now that the demo
   role-switch chips (its previous topbar-right neighbor) relocated into
   the profile popup. Reset the block-layout margin for its new inline
   context; a thin divider + ellipsis keep a long crumb trail from pushing
   the sidebar toggle/brand around on narrow viewports. */
.topbar-left .breadcrumb {
  margin: 0;
  padding-left: 12px;
  border-left: 1px solid var(--border);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* ---- Admin area (THE-42): tabs, tree, drawer, chips ---- */

.admin-tree { display: flex; flex-direction: column; }
.admin-tree-group { border-bottom: 1px solid var(--border); padding: 10px 0; }
.admin-tree-group-header { display: flex; align-items: center; gap: 10px; }
.admin-tree-group-name { font-weight: 600; }
.admin-tree-group-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-quiet); font-size: 12px; font-weight: 400; }
.admin-tree-row { display: flex; align-items: center; gap: 10px; padding: 8px 0 8px 22px; border-bottom: 1px dashed var(--border); font-size: 13px; }
.admin-tree-row:last-child { border-bottom: none; }
.admin-tree-row.inactive { opacity: 0.55; }
.admin-tree-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text-quiet); width: 56px; flex: none; }
.admin-tree-name { flex: 1; }
.admin-tree-owners { display: flex; gap: 4px; align-items: center; }
.admin-tree-meta { color: var(--text-quiet); font-size: 12px; width: 100px; text-align: right; white-space: nowrap; }
.admin-tree-add-row { padding: 6px 0 6px 22px; }
.admin-lock-note { font-size: 11px; color: var(--text-quiet); }

.slug-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-quiet); }

/* Row -> side drawer grammar (DEC-011: list stays visible as context). */
.drawer-overlay { position: fixed; inset: 0; background: rgba(20, 24, 31, 0.35); z-index: 90; }
.drawer-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 380px;
  max-width: 92vw;
  background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: -12px 0 32px rgba(0, 0, 0, 0.18);
  padding: 24px;
  overflow-y: auto;
  z-index: 91;
}
.drawer-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px; }
.drawer-title { font-size: 16px; font-weight: 700; margin: 0; }
.drawer-subtitle { color: var(--text-quiet); font-size: 12px; margin: 0 0 18px; }
.drawer-close { color: var(--text-quiet); font-size: 18px; line-height: 1; }
.drawer-section { margin-bottom: 20px; }
.drawer-section h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-quiet); margin: 0 0 8px; }
.drawer-divider { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
.drawer-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.drawer-danger { color: var(--red); }

.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip-toggle {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 11px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 999px;
  cursor: pointer;
}
.chip-toggle.on { background: var(--accent); border-color: var(--accent); color: white; }
.chip-toggle.locked { opacity: 0.5; cursor: not-allowed; }
.chip-toggle-form { display: inline; margin: 0; }


/* Sponsor Q (2026-08-02, "error message... can it be toast message? when
   click on approve"): retires BOTH prior refusal-display mechanisms at
   once — the inline `.refusal-note` slot (written into whichever
   [data-refusal-host] ancestor the failing element had) and the single
   shared `#action-refused-dialog` modal fallback for elements with no such
   ancestor. One path now, everywhere: layout.ts's htmx:responseError
   handler always shows a toast, stacked in `#toast-region` (fixed
   bottom-right, above the command bar's own z-index). Manual dismiss only
   (no auto-timeout) — this is an error the user needs to read and act on,
   not a fire-and-forget confirmation; the old modal was manual-dismiss
   too, so this isn't a new burden, just a less disruptive one (does not
   block interaction with the rest of the page the way the modal did). */
.toast-region {
  position: fixed;
  right: var(--space-6);
  bottom: var(--space-6);
  z-index: 100;
  display: flex;
  flex-direction: column-reverse;
  gap: var(--space-4);
  max-width: 360px;
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--red);
  border-radius: var(--radius-md);
  padding: var(--space-5) var(--space-6);
  box-shadow: var(--shadow-card);
}
.toast-icon { color: var(--red); flex: none; line-height: 1.4; }
.toast-message { flex: 1 1 auto; font-size: 13px; line-height: 1.4; color: var(--text); }
.toast-close {
  flex: none;
  background: none;
  border: none;
  color: var(--text-quiet);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  padding: 0;
}
.toast-close:hover { color: var(--text); }

.switch { position: relative; display: inline-block; width: 34px; height: 18px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch-track { position: absolute; inset: 0; background: var(--gray-200); border-radius: 999px; transition: background 120ms ease; }
.switch-track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: white; transition: transform 120ms ease; }
.switch input:checked + .switch-track { background: var(--accent); }
.switch input:checked + .switch-track::after { transform: translateX(16px); }

.status-active { color: var(--green); font-size: 12px; }

.numbering-row-scope { display: flex; align-items: center; gap: 6px; }
.numbering-template-input { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; width: 100%; }
.token-chip-row { display: flex; gap: 6px; margin: 8px 0; flex-wrap: wrap; }
.token-chip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  background: var(--gray-100);
  color: var(--gray-700);
  border: none;
  padding: 3px 8px;
  border-radius: 6px;
  cursor: pointer;
}
.numbering-preview { font-size: 12px; color: var(--text-quiet); margin: 8px 0; }
.numbering-preview strong { color: var(--text); }
.numbering-footer-note { color: var(--text-quiet); font-size: 12px; margin-top: 16px; }

.type-confirm-name { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* ---- Help drawer (DEC-021) — third lane, distinct from ⌘K's centered
   modal: a right-side panel that never looks like it can act. ---- */
.help-drawer-overlay {
  position: fixed;
  inset: 0;
  background: transparent;
  pointer-events: none;
  z-index: 90;
}
.help-drawer-overlay[hidden] { display: none; }
.help-drawer {
  position: absolute;
  top: 0;
  right: 0;
  height: 100vh;
  width: 380px;
  max-width: 90vw;
  background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: -8px 0 30px rgba(0, 0, 0, 0.15);
  display: flex;
  flex-direction: column;
  pointer-events: auto;
}
.help-drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
/* #help-drawer-body is the htmx hx-target the /help fragment swaps into
   (layout.ts) — it must itself be the flexing, height-constrained item so
   .help-drawer-inner's own `flex:1; min-height:0` (which lets .help-content
   scroll internally) actually has a bounded parent. Without this rule the
   fragment's natural height pushes .help-ask-row past the drawer's
   height:100vh instead of docking it, for any persona whose help pack is
   long enough to overflow (e.g. Org Admin). */
#help-drawer-body { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.help-drawer-inner { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.help-topics { padding: 12px 16px; border-bottom: 1px solid var(--border); }
.help-search { width: 100%; margin-bottom: 8px; }
.help-topic-list { list-style: none; margin: 0; padding: 0; max-height: 160px; overflow-y: auto; }
.help-topic-list li { display: flex; justify-content: space-between; gap: 8px; padding: 4px 0; font-size: 13px; }
.help-content { padding: 12px 16px; overflow-y: auto; flex: 1; }
.help-topic-section { margin-bottom: 18px; }
.help-topic-section h3 { margin: 0 0 6px; font-size: 14px; }
.help-topic-section p { margin: 0 0 8px; font-size: 13px; }
.help-topic-section ul { margin: 0 0 8px; padding-left: 18px; font-size: 13px; }
.help-ask-row { border-top: 1px solid var(--border); padding: 12px 16px; }
.help-ask-row input { width: 100%; margin-bottom: 6px; }
.help-ask-row button { width: 100%; }

/* ---- Decrypt-error banner (SCR Platform Settings) ---- */
.banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin: 8px 0; }
.banner-error { background: var(--red-tint); color: var(--red); border: 1px solid var(--red); }
/* Project ON_HOLD banner (Slice 2 gate workflow) — reuses the same amber
   tokens as --proxy-banner-*, the app's existing "paused, not broken" color. */
.banner-warn { background: var(--amber-tint); color: var(--amber); border: 1px solid var(--amber); }
/* The third state. A test email that WORKED needs to read as settled, not as
   the absence of a warning — an operator scanning Platform Settings for "is
   mail alive" should get the answer from the colour. Uses the same green pair
   the app already carries for approved/positive states. */
.banner-success { background: var(--green-tint); color: var(--green); border: 1px solid var(--green); }

/* ---- Error page (404 / 403 / 400) ----
   Redesigned 2026-08-12 ("can it be better?"). It was a centred heading, one
   generic sentence and a single "Go home" link, with the styling inline. The
   status is now a quiet number ABOVE the heading rather than inside it: "404"
   is the least useful part for the person reading, and putting it in the <h1>
   made the sentence start with a number. Left-aligned, because the content is
   now a path, a reason and choices rather than one word to be centred. */
.error-page { max-width: 34rem; margin: 12vh auto; padding: 0 var(--space-5); }
.error-page-code { font-size: 12px; letter-spacing: 0.1em; color: var(--text-quiet); margin: 0 0 var(--space-3); }
.error-page-heading { margin: 0 0 var(--space-4); font-size: 22px; }
.error-page-note { color: var(--text-quiet); font-size: 13px; margin: 0 0 var(--space-4); }
/* The URL that failed, so "something is broken" can become "I mistyped this".
   Wraps, because a long ticket URL must not push the page sideways. */
.error-page-path { margin: 0 0 var(--space-5); overflow-wrap: anywhere; }
.error-page-path code { font-size: 12px; color: var(--text-quiet); }
.error-page-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-bottom: var(--space-5); }

/* ---- ⌘K command palette (sponsor screenshot, 2026-08-12) ----
   Every class here shipped with NO rule at all, so the palette rendered as one
   run-on paragraph of links: `.palette-row` is an <a> and its label/sublabel
   are <span>s, all inline, so "Invite member… Members › Invitations · Silicon
   Box Set next number…" ran together with nothing to say where one command
   ended and the next began. Found because the sponsor screenshotted it — and
   ui-lint had never looked at command-center/views. */
.palette-group-label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-quiet);
  padding: var(--space-4) var(--space-5) var(--space-2);
}
/* A ROW, not a link in a sentence. The label and its context stack, so the
   command is what you read and the location is what you confirm. */
.palette-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-4) var(--space-5);
  color: var(--text);
  text-decoration: none;
  border-left: 2px solid transparent;
}
.palette-row:hover,
.palette-row:focus-visible { background: var(--chip-bg); border-left-color: var(--accent); text-decoration: none; }
.palette-row-label { font-size: 14px; }
.palette-row-sublabel { font-size: 12px; color: var(--text-quiet); }

/* The "Ask" affordance sits at the bottom of the list and must read as an
   action rather than another result. */
.palette-ask-row {
  display: block;
  width: 100%;
  text-align: left;
  appearance: none;
  background: none;
  border: none;
  border-top: 1px solid var(--border);
  font: inherit;
  cursor: pointer;
  padding: var(--space-4) var(--space-5);
  color: var(--accent);
}
.palette-ask-row:hover { background: var(--chip-bg); }
.palette-shimmer { padding: var(--space-4) var(--space-5); color: var(--text-quiet); font-size: 13px; }
.palette-proposal-lane:empty { display: none; }

/* The proposal card — the AI half of the palette (DEC-019). Same story: the
   card had a rule, everything INSIDE it did not. */
.proposal-verb { margin: 0 0 var(--space-3); font-size: 14px; }
.proposal-chips { margin: 0 0 var(--space-3); display: flex; flex-wrap: wrap; gap: var(--space-3); }
.proposal-consequence { margin: 0 0 var(--space-4); font-size: 12px; color: var(--text-quiet); }
.proposal-hint { font-size: 12px; color: var(--text-quiet); }
/* Refusals read red; an ambiguity is a question, not a failure, so it does
   not. */
.proposal-denial { margin: 0 0 var(--space-4); font-size: 13px; color: var(--red); }
.proposal-denied { border-color: var(--red); }
.proposal-ambiguous,
.proposal-fallback { display: flex; flex-direction: column; gap: var(--space-3); }
.proposal-candidate {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  width: 100%;
  text-align: left;
  appearance: none;
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  font: inherit;
  cursor: pointer;
  padding: var(--space-3) var(--space-4);
  color: var(--text);
}
.proposal-candidate:hover { border-color: var(--accent); }
/* The number you type to pick it — monospace so 1/7 line up down the list. */
.proposal-candidate-key { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-quiet); font-size: 12px; }
.proposal-success { color: var(--green); font-size: 13px; margin: 0; }

/* Help drawer topic link — the fourth views directory, unscanned until today. */
.help-topic-link { display: block; padding: var(--space-3) 0; color: var(--accent); text-decoration: none; }
.help-topic-link:hover { text-decoration: underline; }

/* ---- Platform Settings tabs (sponsor 2026-08-12) ----
   The page-level tab strip. Its tabs are ANCHORS, not buttons, so the section
   travels in the URL and survives the redirect every form on this page does
   when it saves — with a JS toggle you would set the mail redirect and get
   bounced back to the first tab. `.tab` was written for <button> (appearance,
   background and border reset), and an anchor needs two things that reset does
   not give it: a block box so the vertical padding and the 2px active underline
   size correctly, and no default underline at rest — `.tab:hover` already
   removes it on hover, which without this made the underline appear only when
   the pointer LEFT the tab. */
.settings-tabs .tab { display: inline-block; text-decoration: none; }

/* The agent registry list — a column of cards. Was an inline
   display:flex;flex-direction:column;gap:12px on the wrapper. */
.settings-agent-list { display: flex; flex-direction: column; gap: var(--space-5); margin-top: var(--space-4); }

/* ---- Workspace feature toggle (2026-08-11 sweep) ----
   One row per feature: name on the left, its on/off chip and the verb that
   changes it on the right. Both classes shipped with the screen and had NO
   rules at all — the <form> is block-level and its children inline, so the
   name, the chip and the button ran together as one line of text with a
   space between them, and nothing lined up down the column. */
.feature-toggle-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
}
.feature-toggle-row:last-child { border-bottom: none; }
/* Takes the slack, so every chip and button in the list shares one right
   edge however long the feature names get. */
.feature-toggle-name { flex: 1; }

/* ---- Admin popover form (2026-08-11 sweep) ----
   The little form inside a kebab menu or disclosure — rename, edit code, set
   next number, invite, create workspace. It was hand-spelled as an inline
   style NINE times across five admin views, and in two different sizes:
   padding:8px;gap:8px in some menus, padding:10px;gap:10px in others, for the
   same control in the same kind of container. Nobody chose that; it is what
   copying a style attribute does over time. One class, one size. */
.admin-menu-form { padding: var(--space-4); gap: var(--space-4); max-width: none; }

/* ---- Admin > Invitations (sponsor 2026-08-11: "better layout") ----
   These five classes were in the markup from the day the screen shipped and
   had NO rules at all — ui-lint only scanned web/views, so a dozen admin
   screens went unchecked. The row therefore stacked its action buttons
   vertically, which is what made every live row three lines tall. */

/* The actions cell: one horizontal row that wraps only when it must. Each
   control is its own <form> (htmx posts them independently), and forms are
   block-level, which is precisely why they stacked. */
.invite-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.invite-actions form { display: inline-flex; margin: 0; }

/* Destructive, and the only irreversible control on the screen — it earns red
   on hover rather than at rest, so a list of spent rows is not a wall of
   alarm. The rule itself is grouped with .card-menu-item-danger's, see
   there (2026-08-23 L2 merge). */

.invite-row td { vertical-align: middle; }

/* The role picker sits inline with its Update button rather than on two
   lines. */
.invite-role-form { display: flex; gap: var(--space-2); align-items: center; }

/* Closed invitations, folded away. Every resend revokes its predecessor, so
   this set grows faster than the live one and would otherwise bury it. */
.invite-closed { margin-top: var(--space-4); }
.invite-closed > summary {
  cursor: pointer;
  color: var(--text-quiet);
  font-size: 13px;
  padding: var(--space-2) 0;
  user-select: none;
}
.invite-closed > summary:hover { color: var(--text); }
.invite-closed[open] > summary { margin-bottom: var(--space-2); }

/* The link-shown-once card. A bordered panel because the link inside it can
   never be recovered — the visual weight is the warning. */
.invite-created-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: var(--space-4);
  max-width: 640px;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.invite-created-card h2 { margin: 0; font-size: 16px; }
.invite-copy-btn { white-space: nowrap; }

/* ---- Markdown Detail editor (md-editor round table, 2026-07-20) ----
   .md-body is the ONE rendering surface for shared-renderer output
   (web/markdown.ts): the Preview tab, the incident/CR Description card, and
   CR Business justification. Token-only, both theme bindings — reuses
   --chip-bg/--chip-text (already dark-contrast-audited, Fix 10 ux-cycle1)
   for code/fence backgrounds rather than a raw gray ramp value (the exact
   B1-class mistake this file's own header comment warns against). */
.md-body { color: var(--text); }
.md-body p { margin: 0 0 10px; }
.md-body p:last-child { margin-bottom: 0; }
.md-body strong { font-weight: 700; }
.md-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  background: var(--chip-bg);
  color: var(--chip-text);
  padding: 1px 5px;
  border-radius: 4px;
}
.md-body pre {
  background: var(--chip-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 0 0 10px;
  overflow-x: auto;
}
.md-body pre code { background: none; color: var(--text); padding: 0; border-radius: 0; font-size: 0.85em; }
.md-body ul { margin: 0 0 10px; padding-left: 20px; }
.md-body li { margin-bottom: 4px; }
.md-body.quiet-note, .md-body .quiet-note { color: var(--text-quiet); }

/* ---- Magazine reading tier (Wave 4, linear-refinement, 2026-07-20) — a
   reading TIER inside the existing .md-body surface, not a separate mode:
   Description card + CR Business justification only (Description card +
   the DEC-023-day ruling extending it to Business justification). Styles
   only what the shared renderer (web/markdown.ts) actually emits — p, ul/
   li, strong, code, pre — no phantom h1/blockquote rules. Token-only (no
   new colors): inherits --text/--chip-bg/--chip-text, already dual-theme
   AA-audited (Fix 10 ux-cycle1), so no separate PRN-05 color pass is
   needed here — only the type-scale/measure/rhythm values are new. ---- */
.md-body--reading { font-size: 16px; line-height: 1.7; max-width: 68ch; }
.md-body--reading p { margin: 0 0 16px; }
.md-body--reading ul { margin: 0 0 16px; padding-left: 22px; }
.md-body--reading li { margin-bottom: 8px; }
.md-body--reading pre { padding: 14px 16px; margin: 0 0 16px; }
.md-body--reading code { font-size: 0.85em; }

/* ---- Editorial ticket-detail header (Wave 4) — case number as a quiet
   mono eyebrow (the FULL identifier; the breadcrumb's last crumb keeps the
   short form, owner ruling), subject promoted to the reading tier's title
   size, a quiet meta line beneath. Token-only; no serif, no new colors. ---- */
.ticket-header-editorial { align-items: flex-start; }
/* 2026-07-24 follow-up ("Length of incident & CR subject should be
   flexible"): without flex:1/min-width:0 this column never grows past its
   own content width inside .page-header's flex row, so the subject
   input/h1 (width:100%) collapsed to a narrow default and clipped any long
   subject. Shared by both Incident and CR (same .ticket-header-editorial
   markup). */
.ticket-header-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
.ticket-eyebrow {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-quiet);
  margin: 0;
}
/* Two classes (not `.page-header h1`'s class+type) so this deliberately
   outranks the generic .page-header h1 (24px/500) rule regardless of
   source order — CSS specificity, not cascade position, decides here. */
/* overflow-wrap: defensive, for the READ-ONLY rendering only (a viewer
   with no edit right sees `.ticket-subject-value`, a plain <span>, not
   `.ticket-title-input` — the editable case is already correctly
   width-constrained and needs no help). A subject with no spaces (a
   pasted URL, a single long token) has nowhere the browser's default
   wrapping can break it, so an unwrapped span would run past the card
   edge. Added defensively during a negative-input UI review (tick 127)
   that surfaced no live instance of this actually happening, only the
   gap in the CSS. */
.ticket-header-editorial .ticket-title { font-size: 28px; font-weight: 600; line-height: 1.25; margin: 2px 0 0; overflow-wrap: break-word; }
.ticket-meta { font-size: 13px; color: var(--text-quiet); margin: 4px 0 0; }

/* 2026-07-24 follow-up ("Shrink a subject and progress bar on Scroll") —
   a compact bar (small subject + mini stepper, no eyebrow/meta) that
   appears once scrolled past the real header. First attempt made the
   REAL header itself position:sticky and shrunk its own box on scroll —
   reproducibly broken: a sticky element's reserved flow-space is
   recomputed from its CURRENT (shrunk) size, so by the time you'd
   scrolled further than that shrunk size, the browser treated it as
   "already scrolled past" for layout purposes (pushing
   .incident-reading-layout up to right after the shrunk box's tiny
   static-position slot) while STILL visually pinning it at the top for
   painting purposes — the two calculations desync, and reading-column
   content renders overlapping/above the "stuck" header. Root cause:
   never let a position:sticky element's OWN box height change based on
   scroll.
   Fix (owner-confirmed tradeoff: a small permanent gap over a JS-managed
   spacer): the anchor (.ticket-sticky-header) reserves a CONSTANT slot,
   present from page load and NEVER toggled by scroll — so it can never
   desync from its siblings, and it genuinely has real, always-empty
   space to occupy once visible, rather than overlapping whatever
   content happens to be there. Sized tight (32px) to the compact bar's
   own single-row content, not the old 44px, so the always-present gap
   at the top of the page (before any scrolling) doesn't read as an
   oversized, out-of-rhythm blank band next to the surrounding 20-ish-px
   spacing (2026-07-24 follow-up: "there is a gap ... pls check"). The
   actual visible bar (.ticket-sticky-header-inner) is absolutely
   positioned inside that anchor and toggled by opacity/visibility (not
   display/height), so its own transitions never touch layout either.
   The real, full-size header above stays completely unchanged, normal
   flow, scrolls away exactly as before. z-index 30 — above ordinary
   content, below the drawer/quick-search overlays (90+) and native
   <dialog> top-layer. JS toggle in layout.ts's shared script (scroll
   listener on .content, passive).
   2026-07-24 follow-up ("still have the GAP between shrink bar... can it
   move to closer to the header bar?"): top:-32px (not 0) cancels
   .content's own 32px top padding — sticky's "top" offset is measured
   from .content's PADDING-box edge, so top:0 parked the bar 32px below
   the topbar, with a visible blank gap between them. -32px pulls it up
   into that padding area so it sits flush against the topbar once stuck.
   2026-07-24 follow-up 2 (multi-role UI test sweep): a real bug found —
   combining that top:-32px trick with a NEGATIVE margin-top (an earlier
   attempt to also shrink the always-present page-load gap) made the
   anchor's VISUAL bottom edge (fixed at .content's padding-box top + 32
   once stuck, independent of any preceding sibling's margin) sit BELOW
   where the reading-layout grid's STATIC reserved position actually
   started — the two are computed independently once top and margin-top
   both shift things, and margin-top's negative value was cancelling
   more than the true safety margin allowed, letting the rail's metadata
   strip render 12-13px into the compact bar's painted area. Fix:
   margin-top: 0 — no cancellation at all. This does mean the always-
   present page-load gap (before any scrolling) is a bit larger again;
   correctness (never overlapping real content) wins over that last bit
   of tightening. */
.ticket-sticky-header { position: sticky; top: -32px; height: 32px; margin-top: 0; z-index: 30; }
.ticket-sticky-header-inner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 4px 0;
  /* 2026-07-24 follow-up (multi-role UI test sweep): content scrolling
     UNDER a sticky bar and disappearing behind it is normal, universal
     sticky-header behavior, not a bug — every real sticky nav does this.
     The actual bug was that the previous "glass" background
     (color-mix at 82% + backdrop-filter blur, owner feedback earlier
     the same day) wasn't reliably opaque-looking enough — scrolled-under
     content (the rail's metadata strip, the Activity feed) stayed
     legible enough to read as garbled overlapping text instead of
     cleanly hidden. A sticky bar's background MUST fully obscure
     anything behind it, for any scroll depth (there is no margin/height
     trick that bounds how much content ends up "behind" it as scrolling
     continues — that's inherent to position:sticky, not fixable by
     layout math). Fully opaque var(--bg), no transparency, no blur. */
  background: var(--bg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-6px);
  transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), visibility 0.2s, border-color 0.2s ease-out;
  border-bottom: 1px solid transparent;
}
.ticket-sticky-header.is-compact .ticket-sticky-header-inner {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
  border-bottom-color: var(--border);
}
/* The mobile stepper SUMMARY is hidden in the sticky copy, like the label and
   the skip button beside it. .ticket-sticky-header-inner is absolutely
   positioned inside a fixed 32px anchor, so a two-line summary block overflows
   the bar and steals room from the ellipsised title. */
.ticket-sticky-header-inner .stepper-compact { display: none; }
/* The inert wrapper around the sticky bar's duplicate stepper. Contributes no
   box of its own — it exists to carry `inert`, and must not disturb the flex
   row it sits in. */
.ticket-sticky-header-inner .ticket-eyebrow { margin: 0; flex: none; }
.ticket-sticky-header-inner .ticket-title { font-size: 15px; margin: 0; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ticket-sticky-header-inner .stepper-circles { flex: 1 1 auto; margin: 0; max-width: 420px; }
.ticket-sticky-header-inner .stepper-circle-item:not(:first-child)::before { top: 9px; }
.ticket-sticky-header-inner .stepper-circle { width: 18px; height: 18px; font-size: 9px; }
/* 2026-07-24 follow-up ("should keep the edit toggle on shrink bar as
   well") — status chip + mode toggle, pinned to the bar's right edge. */
.ticket-sticky-header-inner .ticket-header-actions { flex: none; margin-left: auto; }
@media (prefers-reduced-motion: reduce) {
  .ticket-sticky-header-inner { transition: opacity 0.01ms, visibility 0.01ms; transform: none; }
}
.ticket-sticky-header-inner .stepper-circle-label { display: none; }

/* 2026-07-23 follow-up ("subtitle of CR can change anytime", refined
   same-day: "if under edit mode... don't need a save button") — an
   edit-mode-only subject field (data-edit-only) styled to read like the
   static title it replaces (same size/weight); auto-submits on blur/Enter
   (data-auto-submit-on-blur), no separate Save button. */
.ticket-subject-form { margin: 2px 0 0; }
.ticket-title-input {
  font-size: 28px;
  font-weight: 600;
  line-height: 1.25;
  border: 1px solid transparent;
  background: transparent;
  border-radius: 6px;
  padding: 2px 6px;
  margin: 0 -6px;
  color: var(--text);
  min-width: 0;
  width: 100%;
}
.ticket-title-input:hover, .ticket-title-input:focus { border-color: var(--border); background: var(--surface-hover); outline: none; }

.ticket-header-actions { display: flex; align-items: center; gap: 10px; }
/* Prev/Next ticket nav (ui-framework ruling, 2026-08-24, approved wireframe).
   Wrapper only — members are bare `.icon-btn`; `.icon-btn.is-disabled` reuses
   `.list-pager-btn.is-disabled`'s exact idiom (below) for the no-neighbour
   state. border-radius/margin logic below is scoped to `.icon-btn` children
   of THIS wrapper only, so it cannot leak into any other `.icon-btn` call
   site. */
.ticket-header-nav { display: inline-flex; }
.ticket-header-nav .icon-btn:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.ticket-header-nav .icon-btn:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.ticket-header-nav .icon-btn:not(:first-child) { margin-left: -1px; }
.ticket-header-nav .icon-btn:hover, .ticket-header-nav .icon-btn:focus-visible { position: relative; z-index: 1; }
.icon-btn.is-disabled { opacity: 0.45; pointer-events: none; }
/* Structural break above the stepper — rendered only when a stepper exists
   (ticketHeader() guards this; create/draft callers pass no stepperHtml and
   get no divider). `--border`, not accent: every other structural hairline
   in this system uses it, and `--accent` is reserved for actionable/
   significant things. No margin-top: `.page-header` already carries
   margin-bottom:16px, and stacking a second top margin is the per-element
   spacing bug CLAUDE.md warns against. */
.ticket-header-divider { height: 1px; background: var(--border); border: none; margin: 0 0 var(--space-5); }
/* The two data-page-mode visibility rules (2026-07-23) lived here until
   2026-08-07 — header review §8 round B/D retired the Edit toggle, so
   nothing flips data-page-mode off "display" any more. This sheet has had no
   [data-view-only] selector since; MS-30 C1 (2026-08-08) then stopped
   editableSection EMITTING the attribute at all, so neither the reader nor
   the writer survives. Every section is always visible, which is exactly what
   a page with no edit mode means. */
/* Help drawer reuses the same shared renderer (web/markdown.ts) — same
   code/fence treatment for parity if a pack ever adds a fence. */
.help-topic-section pre { background: var(--chip-bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
.help-topic-section code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; background: var(--chip-bg); color: var(--chip-text); padding: 1px 5px; border-radius: 4px; }
.help-topic-section pre code { background: none; padding: 0; }

/* Write/Preview segmented control + expand-to-modal, on Detail and Business
   Justification (ticket-new.ts / intake.ts AI-prefilled fragment). Reuses
   the existing .tabs/.tab recipe (already dual-theme audited) instead of a
   new tab component. */
.md-field-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.md-expand-btn { padding: 2px; line-height: 0; }
.md-tabs { margin-bottom: 8px; }
.md-panel textarea { width: 100%; }
.md-hint { margin-top: 6px; }

/* Expand-to-modal: centered ~720px native <dialog>, styled from the same
   scrim family as the ⌘K quick-search modal (the aria-modal=true, centered-
   canvas precedent — NOT the side drawer, whose grammar is "explains, never
   acts" / narrow row-context). box-shadow reuses --shadow-card (already
   dual-theme audited); ::backdrop reuses the literal scrim value already
   used identically by .quick-search-overlay. */
.editor-modal {
  width: 720px;
  max-width: 92vw;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-card);
}
.editor-modal::backdrop { background: var(--backdrop); }
.editor-modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; gap: 12px; }
.editor-modal .md-panel-modal-write textarea,
.editor-modal .md-panel-modal-write { width: 100%; }
.editor-modal .md-panel-modal-write textarea { min-height: 320px; resize: vertical; }
.editor-modal .md-body { min-height: 320px; }

/* Command bar tidy-up (2026-07-23 follow-up): a smaller sibling of
   .editor-modal for the Comment/Reopen popups (renderFieldDialog,
   ticket-detail-shared.ts) — one plain rich-text field + a submit button,
   not a full write/preview editor, so a narrower card fits the content. */
.action-dialog {
  width: 440px;
  max-width: 92vw;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-card);
}
.action-dialog::backdrop { background: var(--backdrop); }
.action-dialog-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; gap: 12px; }
.action-dialog form { display: flex; flex-direction: column; gap: 10px; }
/* 2026-07-23 follow-up: the attachment-preview dialog needs real room for
   a document/spreadsheet-shaped preview, not the single-field-form width
   the base .action-dialog was sized for. */
/* [open] here is load-bearing, not decoration: a bare `display: flex`
   would override the UA stylesheet's `dialog:not([open]) { display:
   none }` default (any author rule wins over UA origin regardless of
   specificity), leaving the dialog permanently visible even before
   showModal() is ever called — caught live, this rendered "Activity"
   open on first page load with no interaction. */
.action-dialog--wide[open] { width: 720px; max-height: 80vh; display: flex; flex-direction: column; }
.action-dialog--wide .attachment-preview-body { overflow-y: auto; }
/* Confirm-delete's Delete/Cancel pair reads better side by side than the
   single-field forms (Comment/Reopen/Reassign) this base rule was built for. */
.action-dialog form[data-confirm-delete-form] { flex-direction: row; justify-content: flex-end; }

/* ---- Settings dialog (DEC-026, owner ruling 1: dialog card + scrim) —
   native <dialog> ~880×640, dressed as a card over a dimmed backdrop, same
   family as .editor-modal (::backdrop scrim, box-shadow: var(--shadow-card))
   but its own class since the internal layout (200px sidebar + independent-
   scroll content pane) is structurally different from the single-panel
   editor modal. Esc: md-editor-field.ts's stopPropagation guard, reused
   verbatim by settings.ts's own inline script — zero new document-level
   listeners. ---- */
.settings-dialog {
  width: min(880px, 100vw - 48px);
  height: min(640px, 100vh - 96px);
  max-width: none;
  max-height: none;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 0;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-card);
}
/* [open]-scoped, same load-bearing reason spelled out on .action-dialog--wide
   above: an unconditional `display` beats the UA stylesheet's
   dialog:not([open]) { display: none } regardless of specificity, and the
   dialog then renders permanently, in document flow, before showModal() is
   ever called. This one had the bug latent — found 2026-08-10 by the guard
   written after the SAME mistake was made a third time, on the rich-text
   expand dialog. Three occurrences is not carelessness, it is a rule that
   needed a test rather than a comment. */
.settings-dialog[open] { display: flex; }
.settings-dialog::backdrop { background: var(--backdrop); }
.settings-dialog-grid { display: flex; width: 100%; height: 100%; min-height: 0; }
.settings-sidebar {
  width: 200px;
  flex: none;
  padding: 16px 12px;
  border-right: 1px solid var(--border);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.settings-sidebar-link {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 10px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
}
.settings-sidebar-link:hover { background: var(--sidebar-hover-bg, var(--gray-100)); text-decoration: none; }
.settings-sidebar-link.active {
  background: var(--sidebar-hover-bg, var(--gray-100));
  box-shadow: inset 3px 0 0 var(--accent);
  font-weight: 600;
}
.settings-content {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 32px;
}
.settings-content-inner { max-width: 560px; }
.settings-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.settings-title { font-size: 15px; font-weight: 600; margin: 0; }
.settings-close-btn {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  color: var(--text-quiet);
}
.settings-close-btn:hover { background: var(--gray-100); }
.settings-row { display: flex; flex-direction: column; gap: 2px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.settings-row-label { font-size: 12px; color: var(--text-quiet); }
.settings-row-value { font-size: 14px; color: var(--text); }
.settings-membership-list { list-style: none; padding: 0; margin: 8px 0 0; display: flex; flex-direction: column; gap: 6px; }

@media (max-width: 768px) {
  .settings-dialog {
    width: 100vw;
    height: 100dvh;
    max-width: 100vw;
    max-height: 100dvh;
    border-radius: 0;
    top: 0;
    left: 0;
    margin: 0;
  }
  .settings-dialog-grid { flex-direction: column; }
  .settings-sidebar {
    width: 100%;
    height: 44px;
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding: 6px 12px;
    align-items: center;
  }
  .settings-sidebar-link { flex: none; white-space: nowrap; }
  .settings-content { padding: 20px; }
}

/* ---- DEC-028 in-proxy indicator system — compact per owner ruling 4
   (overrides the panel's loud 40px banner): a slim, non-dismissible strip
   with an accent top border + a small pill, still present on EVERY page
   (the compliance floor), just lower visual weight. Reuses --proxy-banner-*
   (aliased to the already-audited --amber pair, see :root). ---- */
.proxy-strip {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 4px 16px;
  background: var(--proxy-banner-bg);
  border-bottom: 3px solid var(--proxy-banner-accent);
  font-size: 12px;
  flex: none;
}
.proxy-strip-pill {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--proxy-banner-text);
  font-weight: 500;
}
.proxy-strip-exit {
  background: var(--proxy-banner-accent);
  color: var(--white, #fff);
  border: none;
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.proxy-strip-exit:hover { opacity: 0.9; }

/* Sidebar avatar during proxy: the EFFECTIVE user's initial (the sidebar
   must stay honest about whose data is on screen) wrapped in a dashed ring
   — the SECOND dashed-ring surface in the product (the first is
   .chip-contact/DEC-010) but a distinct color family (--proxy-banner-accent,
   not --gray-500) since the two rings mean different things: Contact
   on-behalf-of vs proxied identity. */
.user-avatar.avatar-proxy-ring {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--proxy-banner-accent);
  outline: 2px dashed var(--proxy-banner-accent);
  outline-offset: 3px;
}
.account-menu-proxy-header { padding: 6px 8px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
.account-menu-proxy-acting { font-size: 12px; font-weight: 600; color: var(--proxy-banner-text); }
.account-menu-proxy-real { font-size: 11px; color: var(--text-quiet); }
.account-menu-exit-proxy .card-menu-item { color: var(--proxy-banner-text); }

.chip-proxy { background: var(--proxy-banner-bg); color: var(--proxy-banner-text); }

/* Admin user directory (DEC-027 start surface) — the reason confirm form
   revealed by the row's <details>/<summary>, same disclosure mechanism as
   the account-menu popup. */
.proxy-start-details { position: relative; }
.proxy-start-details summary { list-style: none; cursor: pointer; }
.proxy-start-details summary::-webkit-details-marker { display: none; }
.proxy-start-form {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 5;
  width: 280px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.proxy-start-form textarea { width: 100%; }

/* The Delivery Manager's release decision (sponsor 2026-07-27: "DM needs
   manually review if UAT attachment is ok"). Deliberately quiet rather than a
   warning colour — this is context to read before acting, not an error.
   Rendered in the reading column directly after the Deployment section
   (2026-08-02: moved out of the command bar, "leave only action button on
   the command bar.. no confusion" — was sharing the bar with Approve/Reject,
   reading as a third control at a glance), so the evidence and the decision
   are still one screen apart, just no longer crowding the same row. */
.dm-evidence {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 0 0 10px;
  background: var(--surface);
  font-size: 12.5px;
  line-height: 1.45;
  max-width: 520px;
}
.dm-evidence-head { margin: 0 0 4px; font-weight: 600; color: var(--text); }
/* `.dm-evidence-signoff` is RETIRED (2026-08-13). It was a private alias for
   `.quiet-note`: same colour token, 4px less bottom margin, and no font-size at
   all — so it inherited this panel's 12.5px instead of the 12px every other
   quiet line in the app uses. A second name for one look, and the size drift
   was accidental rather than chosen. */
.dm-evidence-files { margin: 0; padding-left: 18px; color: var(--text-quiet); }
.dm-evidence-files li { margin: 1px 0; }
/* The absence of evidence is the one thing here that IS a warning: the DM is
   being asked to release a change nobody attached proof for. Also used for a
   sign-off carrying no note — the note has been mandatory since 2026-07-29, so
   an empty one means a row from before that, and the DM should see the gap. */
/* --red, not --danger: `--danger` was never defined anywhere in this file, so
   this declaration was invalid at computed-value time and the warning rendered
   in the inherited colour — a warning that did not look like one, silently,
   for as long as it existed. Found by tools/ui-lint.mjs, which exists because
   of it. --red carries both theme bindings (#c0392b / #e0685a). */
/* NAMED FOR THE PATTERN, not the panel (2026-08-13). This is the app's ONE
   "absence stated as danger" — verified as the only `--red` + weight-600 pairing
   in this file — and it was hidden behind a DM-evidence-specific name, so the
   next screen that needs to say "the thing that should be here is missing, and
   that matters" would have invented a tenth vocabulary rather than found this
   one. No other site adopts it today; that is the point of naming it now. */
.absence-warning { margin: 0; color: var(--red); font-weight: 600; }

/* The sign-off note itself (sponsor 2026-07-30: the DM reads the note, not just
   who signed). Quoted rather than plain, because it is somebody's words. */
.dm-evidence-note {
  margin: 0 0 6px;
  padding: 0 0 0 8px;
  border-left: 2px solid var(--border);
  color: var(--text);
  white-space: pre-wrap;
}
/* Deliberately quiet: the note is the content, the edit marker is provenance. */
.dm-evidence-amended { color: var(--text-quiet); font-size: 11.5px; white-space: normal; }

/* Multi-line CR quick-entry on an incident (2026-07-27). One incident routinely
   spawns several change requests, and the old form took one subject per submit
   behind a collapsed disclosure — three CRs meant three round trips and
   re-opening the disclosure between each. Typing was never the slow part.
   Always open, one item per line, Cmd/Ctrl+Enter to submit. */
/* Header review §8 round B (2026-08-07): the form was `data-edit-only` (the
   retired Edit toggle's last gate besides the command bar); it now sits
   inside a `.linked-cr-create-details` click-to-reveal instead. The 2026-07-27
   "always open" note above described the form WITHIN edit mode — the
   disclosure this replaces is the page-wide mode flip, not the per-widget
   one that note removed. The native marker is hidden the same way every
   other summary-as-button in this file does it. */
.linked-cr-create-details > summary { list-style: none; }
.linked-cr-create-details > summary::-webkit-details-marker { display: none; }
.linked-cr-create-details[open] > summary { margin-bottom: var(--space-3); }
.linked-cr-create-form { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.linked-cr-create-form textarea {
  width: 100%;
  min-height: 52px;
  resize: vertical;
  font: inherit;
  line-height: 1.5;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
}
.linked-cr-create-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.linked-cr-hint { font-size: 11.5px; color: var(--text-quiet); }
@media (max-width: 768px) {
  /* 16px prevents iOS Safari zooming the viewport on focus — that zoom is what
     makes "just add another one" feel slow on a phone, because every entry
     costs a pinch back out. */
  .linked-cr-create-form textarea { font-size: 16px; min-height: 64px; }
  .linked-cr-create-actions .btn { min-height: 44px; }
}

/* Linked-CR rollup on an incident (2026-07-27). One incident now raises several
   CRs at once; without this you had to read every chip and total them yourself
   to answer "where has this incident got to". Shown only from two CRs up — a
   single one needs no summary, its own chip says it all. */
.cr-rollup { margin: 2px 0 10px; }
.cr-rollup-line { display: flex; align-items: baseline; gap: 6px; font-size: 12.5px; color: var(--text-quiet); }
.cr-rollup-count strong { color: var(--text); font-variant-numeric: tabular-nums; }
.cr-rollup-quiet { font-size: 11.5px; }
.cr-rollup-track {
  margin-top: 5px;
  height: 4px;
  border-radius: 999px;
  background: var(--border);
  overflow: hidden;
}
.cr-rollup-fill { height: 100%; background: var(--accent); border-radius: 999px; transition: width 200ms ease-out; }

/* The ticket command bar is bottom-docked, so on a phone it sits ON TOP of
   whatever the page ends with. On an incident that ends with the CR quick-entry
   field, it covered the Create button outright — you could type three change
   requests and have no way to submit them (found 2026-07-27 by looking at the
   mobile render, not by a test; nothing about the markup is wrong, the two
   elements simply occupy the same pixels).
   Reserving space below the last section is the fix rather than moving either
   element: both belong where they are. */
@media (max-width: 768px) {
  .incident-detail-page .reading-column > :last-child,
  .cr-detail-page .reading-column > :last-child {
    margin-bottom: 96px;
  }
  /* §8 round 28 (GATE-19 phase 4): the 96px command-bar clearance is gone
     with the bar — the Activity section keeps a normal breathing margin. */
  .incident-detail-page .ticket-activity-section,
  .cr-detail-page .ticket-activity-section {
    margin-bottom: var(--space-6);
  }
}

/* "Use solution" — fills the CR quick-entry from the accepted solution's own
   lines. Quiet and secondary: it is an accelerator, not the primary action, and
   a convenience button that shouts competes with the Create it sits beside. */
.cr-use-solution {
  background: none; border: 0; padding: 0; margin-left: 10px;
  font: inherit; font-size: 11.5px; color: var(--accent-text, var(--accent));
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.cr-use-solution:hover { text-decoration-thickness: 2px; }
@media (max-width: 768px) {
  /* On a phone this is the whole point — no typing at all — so it gets a real
     touch target rather than a text-sized one. */
  .cr-use-solution { font-size: 13px; min-height: 44px; display: inline-flex; align-items: center; }
}

/* Clickable timeline, part 1 — NAVIGATION (sponsor 2026-07-29: "Timeline of CR
   Steps should be able to click navigate to the section for fast entry").
   The step itself is a plain in-page link to its section's anchor. It is a
   real box rather than display:contents so a focus ring has something to draw
   around: the step is keyboard-reachable (a native <a>), and a keyboard path
   whose focus nobody can see is not a keyboard path — the same conclusion the
   column-picker grip reached. Vertical padding stays 0 so the circle sits at
   exactly the height .stepper-circle-item's connecting ::before assumes. */
.stepper-step-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  min-width: 0;
  padding: 0 4px;
  border-radius: 8px;
  color: inherit;
  text-decoration: none;
}
.stepper-step-link:hover .stepper-circle-label { color: var(--text); }
.stepper-step-link:hover .stepper-circle { border-color: var(--accent); }
.stepper-step-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The landing offset for those links. The sticky bar paints over the top of
   the scroll container, so an anchor scrolled flush to the top would land
   UNDER it; scroll-margin-top reserves the bar's slot plus a little air.
   Zero-height, so it costs the reading column no space. */
.section-anchor { display: block; height: 0; scroll-margin-top: 84px; }
/* It is a scroll/focus TARGET, not a control — it is script-focused after a
   step click so the keyboard caret follows the scroll, and never reachable by
   Tab. A focus ring on a zero-height invisible div would read as a stray
   line across the page, and there is nothing there to outline. */
.section-anchor:focus { outline: none; }

/* Clickable timeline, part 2 — SKIP AHEAD. Only UPCOMING phases reachable
   through ungated edges get one. It used to BE the step (the whole circle was
   the submit button), which left nowhere for the navigation above to live and
   meant a click meant as "show me that section" silently moved the ticket's
   status instead. It is now a separate, quieter control underneath the label:
   two different actions, two different targets. Hidden in the sticky bar,
   which is a single 32px row of scroll chrome with no space for a second line
   — the full stepper a scroll away still offers it. */
.stepper-jump {
  margin-top: 4px;
  background: none;
  border: none;
  padding: 1px 5px;
  border-radius: 999px;
  font-family: inherit;
  font-size: 9px;
  line-height: 1.3;
  letter-spacing: 0.02em;
  color: var(--text-quiet);
  cursor: pointer;
}
.stepper-jump:hover { color: var(--accent); }
.stepper-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.stepper-circle-item.is-jumpable .stepper-circle { border-style: dashed; }
.stepper-circle-item.is-jumpable:hover .stepper-circle {
  border-style: solid;
  border-color: var(--accent);
}
.stepper-circle-item.is-jumpable form { display: contents; }
.ticket-sticky-header-inner .stepper-jump { display: none; }

/* Toolbar popovers (customisable-table-v2 D10): Views / Filters / Columns /
   Settings each get their own trigger instead of being tabs in one dialog.
   Native <details>, so layout.ts's existing click-outside handler closes them
   for free. Right-aligned by default because they hang off the toolbar's
   right edge and would otherwise overflow the frame. */
.list-pop { position: relative; }
.list-pop-summary { list-style: none; cursor: pointer; }
.list-pop-summary::-webkit-details-marker { display: none; }
.list-pop-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 260px;
  max-width: min(360px, calc(100vw - 32px));
  max-height: 70vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-card), 0 12px 32px rgba(0, 0, 0, 0.16);
  padding: 12px;
}
.list-pop-panel.align-left { right: auto; left: 0; }
.list-density-toggle[aria-pressed='true'] { color: var(--accent-text); border-color: var(--accent); }

/* Searchable view field (customisable-table-v2 T2). One markup, two
   presentations: a dropdown under the field on desktop, a bottom sheet at
   mobile widths — duplicating the list for mobile would mean two things to keep
   in step. */
.view-search { position: relative; }
.view-search-summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  cursor: text;
  list-style: none;
}
.view-search-summary::-webkit-details-marker { display: none; }
.view-search[open] .view-search-summary { border-color: var(--accent); }
.view-search-input {
  border: none;
  background: none;
  outline: none;
  padding: 0;
  font: inherit;
  color: var(--text);
  width: 140px;
  min-width: 0;
}
.view-search-panel { min-width: 280px; }
@media (max-width: 768px) {
  /* Bottom sheet: a 280px dropdown anchored to a right-edge control is
     unusable on a phone. Same panel, promoted to full width. */
  .view-search-panel {
    position: fixed;
    inset: auto 0 0 0;
    max-width: none;
    max-height: 70vh;
    border-radius: 12px 12px 0 0;
    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.24);
  }
  .view-search-input { width: 100px; }
}

/* Settings popover now holds two per-user sections (search fields + rows per
   page), so they need separating. Token-only — --border is bound in :root, the
   [data-theme='dark'] block and the prefers-color-scheme duplicate, so both
   theme bindings hold. */
.settings-section + .settings-section {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.page-size-select { width: 100%; }

/* Generic full-width <select> for a .settings-form popover control — the
   Module filter (2026-08-27) is this rule's second caller, alongside
   .page-size-select above, which stays as its own literal name rather than
   being repointed here: this file's owner (ui-framework) doesn't edit
   ticket-list.ts callers, and a rename with only one caller moved is not
   worth the diff. Any THIRD select that just needs full width should use
   .settings-select, not invent its own -select class (rule 4: two real
   callers justify the generic name; a third repeating the literal instead
   would be the debt this rule exists to avoid). */
.settings-select { width: 100%; }

/* The column grip is now a real control (focusable, role=button, arrow-key
   reorder), so it needs a visible focus ring and a usable hit area — a
   keyboard path nobody can see the focus of is not a keyboard path. Matches
   .table-scroll:focus-visible so it inherits an already-audited token. */
.col-drag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  min-height: 24px;
  cursor: grab;
}
.col-drag:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Keeps the locked (Case/Subject) row aligned with the wider grips above. */
.col-drag-spacer { width: 20px; display: inline-block; }

/* Private saved views (V2). The chip is the only thing distinguishing a view
   nobody else can see from one the whole workspace shares, so it uses the
   accent tint rather than the neutral chip tone. */
.saved-view-private { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-quiet); }

/* Density toggle glyph — the button shows the density clicking would GIVE you,
   matching its aria-label. aria-pressed is the single source of truth for both
   (set by the same click handler), so the icon can never disagree with the
   state the button reports to assistive tech. */
.list-density-toggle .density-ico { display: none; line-height: 0; }
.list-density-toggle[aria-pressed='false'] .density-ico-compact { display: inline-flex; }
.list-density-toggle[aria-pressed='true'] .density-ico-comfortable { display: inline-flex; }

/* Development checklist (sponsor 2026-07-29: "Table of Check list (optional)
   ... so can also check the process of CR Dev progress"). Deliberately thin:
   the table itself is the generic .table recipe, so only the three things that
   recipe cannot know are declared here — the fixed column widths (.table is
   table-layout: fixed, so the tick and remove columns must be sized or they
   take a third of the row each), the tick affordance, and the progress meter.
   Colors are existing tokens only (--accent / --border / --surface), each
   bound in BOTH theme blocks, so no dark-mode rule is needed. The bar track is
   --border rather than --gray-100 on purpose: the gray ramp is declared once
   and never re-bound for dark, so a --gray-100 track would glow. */
.dev-checklist-progress { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.dev-checklist-count { font-size: 12px; color: var(--text-quiet); font-variant-numeric: tabular-nums; }
.dev-checklist-bar { flex: 1; max-width: 220px; height: 5px; border-radius: 999px; background: var(--border); overflow: hidden; }
.dev-checklist-bar-fill { display: block; height: 100%; background: var(--accent); }
.dev-checklist-tick-col, .dev-checklist-tick-cell { width: 56px; }
.dev-checklist-actions-col, .dev-checklist-actions { width: 40px; }
.dev-checklist-desc { white-space: normal; overflow-wrap: anywhere; }
.dev-checklist-form { margin: 0; line-height: 0; display: inline-block; }
/* A submit button wearing a checkbox's clothes — see cr-detail.ts for why it
   isn't an <input type=checkbox>. Sized to the 24px touch rhythm the icon
   buttons already use so a thumb can hit it on a phone. */
.dev-checklist-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface);
  color: transparent;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.dev-checklist-tick.is-done { background: var(--accent); border-color: var(--accent); color: white; }
.dev-checklist-tick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Done items recede but stay readable — struck-through-and-greyed at once is
   unreadable in the dark binding. */
.dev-checklist-row.is-done .dev-checklist-desc { color: var(--text-quiet); }
.dev-checklist-add { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
.dev-checklist-input {
  flex: 1;
  min-width: 0;
  padding: 7px 10px;
  font-size: 13px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
}
/* Phone: the add row is the one thing that cramps — a text field and a button
   side by side under ~360px leave no room to type. The table itself is already
   fluid (fixed layout + a wrapping description column), so it needs nothing. */
@media (max-width: 768px) {
  .dev-checklist-add { flex-wrap: wrap; }
  .dev-checklist-input { flex: 1 1 100%; }
  .dev-checklist-add .btn { flex: 1 1 100%; }
  .dev-checklist-bar { max-width: none; }
}

/* --- Requestor tags (sponsor 2026-07-29: "pls add the multiple tag input") ---
   No new colour system: chips reuse the --chip-bg/--chip-text pair that
   .chip-workspace/.chip-status already bind in BOTH themes (PRN-05), so this
   widget inherits the dark binding for free rather than declaring its own.
   The sponsor referenced Tailwind as a look, not a dependency — nothing here
   pulls in a framework. */
.req-chip-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 6px 0 10px; }
/* TOKENFIELD — sponsor 2026-08-09 ("the multiple input tag field can be
   better", with a reference screenshot). ONE box: the chips are the field's
   content, sitting inside its border with the caret after them, the shape
   every user already knows from an email To: line. It replaces a chip row
   ABOVE a separately-bordered input, which was two boxes for one field — the
   chips read as output and the empty input read as the control.

   The box borrows the input recipe (border, radius, --surface) rather than
   inventing one, so it sits in a stack of .field rows as an equal and inherits
   the same focus treatment. `focus-within` is what makes it feel like a single
   control: the border lights on the WRAPPER when the caret is anywhere inside,
   which is the whole illusion. Chip styling is reused verbatim from .req-chip
   — a second chip recipe would be two things to keep in dark-mode sync
   (PRN-05) for no gain. */
/* PANEL 2026-08-09 (Cosmetic findings 1–3). The create screen puts a labelled
   FIELD STACK inside the header card. `.ticket-header-detail-slot` could not
   carry it: that slot is the record's labelless BODY — no border, no padding,
   sized for flowing prose — and a form in it lost `.form`'s gap, so vertical
   rhythm became whatever each child's margin happened to produce.

   This is that container, and nothing more: the same flex-column + `--space-6`
   shape `.form` already uses, so the two agree by construction rather than by
   two numbers someone has to keep in step. It does NOT restyle the fields; they
   stay `.field` / `.field-row`, which is why Reporter and Priority now sit in a
   row together and stop stretching a 6-character value across 818px. */
/* The Subject key (sponsor 2026-08-09, round 4). The same caps micro-label
   grammar as every other field on the card — it is only separate from
   .meta-strip-label because it sits ABOVE the <h1>, outside the props strip,
   and must not inherit that strip's flex layout. Editable-Subject screens only:
   the reading page's title needs no key, because a heading that is not a
   control is not a field. */
.ticket-title-label {
  display: block;
  margin-bottom: 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-quiet);
}
/* The dialog's own toggle row (Incident, 2026-08-10) — the same control the
   rail carries, given a line of its own above the two views. */
.chat-expand-toggle-row { margin-bottom: var(--space-3); }
/* .transport-card and .transport-add-details are GONE (2026-08-22, same
   reasoning as .transport-correct's own deletion note that used to sit here):
   the 2026-08-13 one-card unification (transportRequestCard) replaced the
   incident's hand-built Transport card, ms30-p2-incident.spec.ts asserts the
   old markup absent, and the rules had no caller left. DESIGN-SYSTEM.md's
   debt section had already flagged them "dead CSS, not yet swept". */
.ticket-header-form { display: flex; flex-direction: column; gap: var(--space-6); }
/* A real <form class="form"> nested INSIDE the header card (CR's create form
   — its requestor tokenfield script builds hidden inputs with no form=
   attribute, so its fields must be genuine DOM descendants of the form,
   unlike Incident's out-of-form/form= pattern; see renderNewCrBody's
   architecture note). .form's 560px cap is for standalone page forms (login,
   settings) where nothing else bounds the line length — in here the CARD is
   the width authority, and the cap made CR's fields render ~560px beside
   Incident's ~900px for identical fields on the sibling screen (live audit
   2026-08-21; sponsor picked Incident's width as canonical, 2026-08-22). */
.ticket-header-form .form { max-width: none; }
/* The label half of the same alignment: caps micro-labels, matching the header
   card's property keys exactly (.meta-strip-label). Declared here rather than
   by adding the class in the markup, because these are still real <label for=>
   elements doing a form's job — only their APPEARANCE is borrowed. */
.ticket-header-form .field > label {
  display: block;
  margin-bottom: 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-quiet);
}
/* The Detail editor keeps its own chrome: on the create screen it is an
   ALWAYS-editing field with nothing in it yet, and a discrete empty field is
   indistinguishable from no field at all. Discreteness is for values that can
   be read at rest. */
/* Root Cause classification (sponsor 2026-08-09, panel round 3) — the caps
   micro-label + value pair that every other key inside a communication card
   already uses, laid out inline because the value is one short phrase and a
   stacked label over a 12-character answer wastes a whole row. Reuses
   .meta-strip-label / .meta-strip-value verbatim; the only new thing is the row
   and the select's own compact sizing, which borrows the header card's select
   treatment rather than the form's full-width one. */
.rc-type-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.rc-type-select {
  padding: 3px 8px;
  font-size: 12px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.rc-type-select:hover { border-color: var(--text-quiet); }
.rc-type-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.req-tokenfield {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-height: 38px;
  padding: 6px 8px;
  margin: 6px 0 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
}
.req-tokenfield:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
.req-tokenfield--readonly { background: transparent; border-style: dashed; }
/* The input FLOWS with the chips and takes the leftover width, so a full row
   of chips pushes it to the next line rather than squeezing it to nothing. */
.req-tokenfield-add { display: flex; flex: 1 1 140px; min-width: 140px; margin: 0; gap: 6px; }
.req-tokenfield-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 2px 2px;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 13px;
}
/* No ring on the input itself — the WRAPPER already shows focus, and two
   nested focus rings read as two controls, which is what this replaced. */
.req-tokenfield-input:focus { outline: none; }
.req-tokenfield-input::placeholder { color: var(--text-quiet); }
/* Typed, but not a name this list knows. Borrows .chip-contact's dashed
   vocabulary (DEC-010, the product's one dashed-ring surface) rather than
   inventing a colour: dashed already means "a person the system does not hold"
   everywhere else, and this is the same statement one step earlier. NOT red —
   nothing has failed yet, and an error colour on a half-typed address would
   shout at every keystroke. */
.req-tokenfield-input--unknown {
  color: var(--text-quiet);
  border-bottom: 1px dotted var(--gray-500);
}
/* Quiet by design: the box already says "type here". A prominent Add button
   beside a tokenfield is the second control this redesign removes. Kept in the
   DOM (not display:none) because Enter needs a submit and no-JS needs a
   button. */
.req-tokenfield-submit {
  border: none;
  background: transparent;
  color: var(--text-quiet);
  font-size: 12px;
  font-weight: 600;
  padding: 0 4px;
  cursor: pointer;
  border-radius: 4px;
}
/* Sponsor 2026-08-10 ("can just select or enter?"): once the enhancement
   script is running, picking a suggestion submits by itself and Enter always
   did — so the button is the third way to do the same thing and goes. Hidden by
   the ATTRIBUTE the script sets, never unconditionally: with JS off it is the
   only way to commit, and a box with no commit is worse than a spare button. */
:root[data-tokenfield-enhanced] .req-tokenfield-submit { display: none; }
.req-tokenfield-submit:hover { color: var(--accent); background: var(--accent-tint); }
.req-tokenfield-submit:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.req-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 6px 3px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.5;
  background: var(--chip-bg);
  color: var(--chip-text);
  /* A long display name must wrap the ROW, never widen the rail past it. */
  overflow-wrap: anywhere;
}
/* The reporter is not a tag: no × , and a quiet role word so the difference is
   visible rather than something you learn by clicking. */
.req-chip-reporter { background: var(--accent-tint); color: var(--accent-on-tint); padding-right: 10px; }
.req-chip-role { font-weight: 500; opacity: 0.75; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
.req-chip-form { display: inline-flex; margin: 0; }
.req-chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 24px, not the 16px the glyph needs: this is the one control on the card a
     thumb has to hit, and the sponsor asked for mobile as well as desktop. */
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.65;
}
.req-chip-x:hover { opacity: 1; background: var(--red-tint); color: var(--red); }
.req-chip-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.req-tag-input {
  width: 100%;
  padding: 7px 10px;
  font-size: 13px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
}
.req-add-row { display: flex; gap: 8px; align-items: center; margin-top: 4px; }
.req-add-row select { flex: 1; min-width: 0; }
.req-tags-card h3 { margin-top: 0; }

/* --- Functional / Developer tags (sponsor 2026-07-29: "can assign multiple
   functional to CR", DISPLAY ONLY) ---
   Deliberately ZERO new chip styling: this card reuses .req-chip / .req-chip-x
   / .req-chip-row above verbatim. All it adds is the two-lane heading, because
   the only genuinely new thing here is that one card holds two lists. A second
   chip recipe would be two things to keep in dark-mode sync (PRN-05) for no
   visual gain. */
.contrib-tags-card h3 { margin-top: 0; }
.contrib-lane + .contrib-lane { margin-top: 12px; }
/* Phone: a select and a button side by side under ~360px leave the select too
   narrow to read a name in — same trade the dev-checklist add row makes. */
@media (max-width: 768px) {
  .req-add-row { flex-wrap: wrap; }
  .req-add-row select { flex: 1 1 100%; }
  .req-add-row .btn { flex: 1 1 100%; }
}

/* Deployment's "Submit new TP" (2026-08-02: "instead of popup... can we do
   inline? at Deployment section?", then "it is confusing... just sub
   section to submit the TPs" with a two-plain-boxes-plus-Submit sketch) —
   was a dialog with a multi-row textarea and a rich-text field (toolbar
   and all); now an always-visible inline form under the TP list, two
   single-line inputs, same flex-column/gap shape as .dm-evidence-amend and
   .linked-cr-create-form (this app's established "inline form in the
   reading column" recipe — not a new one). */
/* RETIRED 2026-08-13. Opening a request IS adding its first transport, so the
   section ends with an empty Transport Request card carrying the same Add row
   every request carries — one shape instead of two, and no form left to style.
   (Sponsor: "why transport card is different. can we make it the same? just
   standardize and simplify it.") */

/* The Transport Request amend disclosure (pencil -> ✓/✕ -> Discard/Save, a
   <details> plus :has([open]) rules) is RETIRED, 2026-08-13. The sponsor's
   2026-08-10 ruling — "we don't need edit button for Transport Request card?
   just allow delete and sorting" — reached the Incident card and stopped
   there; aligning the two ("align Transport Request in Incident, CRs") means
   CR edits its list in place too, so there is no second state to style. */
/* The card's own text inputs fill it. CR's used to get their width from
   .deployment-new-submission-form, a class named for a screen the incident does
   not have — so the incident's copy came out at the browser's default input
   width and clipped its own value. Stated once, here, where both callers are:
   since 2026-08-13 there is only one card, and it is this one. */
/* Per-row controls on a transport: move earlier, move later, remove. Quiet
   until the row is hovered or something in it has focus — a list of five
   transports with fifteen permanently visible buttons reads as a control panel
   rather than as the sequence it is. Focus-within, not just hover, so a
   keyboard user can reach them at all. */
/* §17 P2 — the Change Required declaration, on the Solution card. A quiet row
   under the solution text, not a banner: it is one fact about the fix, stated
   beside the fix. The caption says what ticking it DOES, because "change
   required" alone does not tell a reader their incident is about to grow two
   stages. */
.change-required-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px dashed var(--border);
}
.change-required-label { display: inline-flex; align-items: center; gap: var(--space-2); font-size: 13px; font-weight: 600; cursor: pointer; }
/* Sponsor 2026-08-12: "pls align 'Adds UAT and Deployment to this incident.'"
   `.quiet-note` is a block PARAGRAPH recipe and its 10px bottom margin is the
   gap to the NEXT paragraph. As a flex item it has no next paragraph, but the
   row still centres the note's MARGIN box — so the 10px was counted below the
   text and pushed it 5px up, leaving the caption floating above the label it
   annotates. Zeroed here rather than on `.quiet-note` itself: everywhere else
   it really is a paragraph and really does want that gap. */
.change-required-row > .quiet-note { margin-bottom: 0; }
.change-required-note { margin: var(--space-4) 0 0; padding-top: var(--space-3); border-top: 1px dashed var(--border); }
/* THE NUMBERS, restored 2026-08-13. `.tp-sequence` is an <ol> because the order
   IS the import order — the whole reason it stopped being a comma-joined line —
   and `display: flex` on the <li> (added for the hover controls) silently
   dropped every marker, because a flex item is no longer a list-item. So the
   list had been rendering unnumbered for as long as the controls have existed,
   on both record types, and nothing failed. A counter puts them back and
   survives the flex layout. */
.tp-sequence { counter-reset: tp; list-style: none; padding-left: 0; }
.tp-sequence li { display: flex; align-items: center; gap: var(--space-3); counter-increment: tp; }
.tp-sequence li::before {
  content: counter(tp) ".";
  flex: 0 0 auto;
  min-width: 14px;
  color: var(--text-quiet);
}
.tp-ref { flex: 0 0 auto; }
.tp-move-actions { display: inline-flex; align-items: center; gap: 2px; opacity: 0; transition: opacity 0.12s ease-out; }
.tp-sequence li:hover .tp-move-actions,
.tp-sequence li:focus-within .tp-move-actions { opacity: 1; }
.tp-move-form { display: inline-flex; margin: 0; }
.tp-move, .tp-remove { width: 20px; height: 20px; font-size: 12px; line-height: 1; color: var(--text-quiet); }
.tp-move:hover { color: var(--accent); }
/* Removal is the one destructive act in the card, so it is the one control that
   changes colour to say so — and only on hover, where the intent is expressed.
   --red, the product's existing destructive token, dark-audited: an invented
   fallback would silently BECOME the design in one theme, which is the same
   trap .chip-awaiting fell into earlier today. */
.tp-remove:hover { color: var(--red); }
@media (prefers-reduced-motion: reduce) { .tp-move-actions { transition: none; } }

/* The in-card "add one transport" row (sponsor 2026-08-09, "what is
   convenience to user?"). One field, one button, on ONE line — the whole point
   is that appending a transport costs less than opening the whole-list editor
   beside it, and a control that stacks would not read as cheaper than the
   pencil. The input takes the slack so the button stays its natural width. */
/* The request's OWN fields — run window, note to the approver, backout plan —
   in one form with one Save (2026-08-13). They were a row each, which meant
   three Save buttons for three sentences about one request: exactly the
   "standardize and simplify" the sponsor asked for the opposite of. */
.tp-details-form { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-4); }
.tp-details-form > input[type="text"] { width: 100%; }
.tp-details-form > .btn { align-self: flex-start; }
/* The window: a two-option radio plus the datetime it enables. Inline, because
   "Immediately / After <when>" is one sentence and stacking it would read as
   three unrelated controls. */
.tp-when { border: 0; margin: 0; padding: 0; display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.tp-when legend {
  padding: 0;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-quiet);
}
.tp-when-choice { display: inline-flex; align-items: center; gap: var(--space-2); }
/* The whole point of staying at "a record, not a scheduler": a bare timestamp
   on an approved request is exactly the thing a reader assumes will fire. It
   takes the full width so it reads as a caption on the row above, not as a
   fourth control in it. */
.tp-when-note { flex-basis: 100%; margin: 0; }
/* `.tp-optional`/`.tp-optional > summary` etc. (the 2026-08-13 disclosure
   wrapping Run it and the note field) are RETIRED (sponsor, 2026-08-24,
   "Transport Request is too complicated... Run it & Backout plan doesn't
   need to hide") — `.tp-when`/`.tp-when-choice`/`.tp-when-note` above are
   UNCHANGED and describe the fieldset's own internals regardless of the
   wrapper; `.tp-details-form`'s own `gap` (above) now spaces all three
   fields directly, reproducing the disclosure's old child spacing exactly. */

/* The read-only face of those same three fields, for a viewer who may not write
   them (the Delivery Manager deciding, anyone reading a decided request). A
   definition list because that is what it is — three labelled facts. */
.tp-facts { margin: var(--space-4) 0 0; display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-4); }
.tp-facts dt {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-quiet);
  padding-top: 2px;
}
.tp-facts dd { margin: 0; }
/* The DM's remark, under the verdict it belongs to. Quoted, so it reads as
   somebody's words rather than as more of the app's own copy. */
/* Its OWN line under the verdict, not beside it. The decision banner is a flex
   row with space-between (it was built for "verdict … action"), which pushed the
   remark hard right and read as a caption on nothing. */
.tp-decision-remark { flex-basis: 100%; margin: var(--space-2) 0 0; font-style: italic; }
/* The transport card's own decision form is RETIRED (2026-08-13): it is
   `.decision-card` now, the one recipe every decision in this app uses. It had
   stacked the remark above the pair because inline the field came out ~180px —
   a box asking for a sentence, sized for a word. `.decision-card` already
   solves that with `input { flex: 1; min-width: 180px }` and wraps when the row
   runs out of room, so the outlier bought nothing the shared rule did not. */
.tp-append-row {
  display: flex;
  gap: var(--space-4);
  align-items: center;
  margin-top: var(--space-5);
}
.tp-append-row input { flex: 1; min-width: 0; }
/* Hidden while the whole-list editor is open: two transport inputs visible at
   once, one appending and one replacing, is the ambiguity this row exists to
   remove. Mirrors the selector directly above, which hides the amend form
   while it is CLOSED. */

/* ---- Notifications (MS-12) ------------------------------------------------
   A notification is a RECORD THAT SOMETHING HAPPENED — deliberately not an
   .inbox-row, which is a PENDING OBLIGATION carrying split-pane [aria-selected]
   state. Same anatomy (the fix-11 recipe), separate component; the one leaf
   rule whose requirement is byte-identical is shared by widening its selector,
   not by copying it.

   Unread is a dot plus a heavier subject, never a tinted row: a tint fights
   .list-row:hover (--surface-hover is LIGHTER than --accent-tint in the light
   binding and DARKER in the dark one, so hovering an unread row would read as
   "it just became read", in opposite directions per theme), and the normal
   state of this panel after a busy morning is EVERY row unread, where a per-row
   background is just a panel background that distinguishes nothing. A dot is
   presence/absence of a shape, so it is not a colour-only distinction. ------ */

.notif-bell { display: inline-flex; align-items: center; gap: 4px; padding: 6px 8px; }
.notif-bell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Rendered even at zero unread: it is the htmx poll's anchor, and an element
   that deletes itself cannot ask for its own replacement. */
.notif-bell-count { display: inline-flex; align-items: center; }

/* Inherits .list-pop-panel's surface/border/radius/shadow. Overrides only what
   a fixed head + scrolling body + fixed foot needs — without overflow:hidden
   and max-height:none the PANEL scrolls and the header and footer slide away
   with the rows they exist to frame. */
.notif-pop-panel { width: 380px; max-width: min(380px, calc(100vw - 24px)); max-height: none; overflow: hidden; padding: 0; }
.notif-pop-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); }
.notif-pop-title { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.notif-pop-body { max-height: 420px; overflow-y: auto; }
.notif-pop-body .empty-state { margin: 12px; padding: 16px; }
.notif-pop-loading { padding: 16px 14px; margin: 0; }
.notif-pop-foot { padding: 8px 14px; border-top: 1px solid var(--border); text-align: center; }
.notif-pop-foot a { font-size: 13px; color: var(--accent-text); }

.notif-filter { display: flex; gap: 6px; margin-bottom: 16px; }
/* Styled off aria-current, not an .is-active class, so the ARIA state and the
   visual state cannot disagree. */
.notif-filter .btn[aria-current] { background: var(--accent-tint); border-color: var(--accent); color: var(--accent-on-tint); font-weight: 600; }
.notif-list { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
.notif-page-foot { display: flex; justify-content: center; margin-top: 16px; }

/* align-items:flex-start because this row is three lines tall, not one. */
.notif-row { position: relative; align-items: flex-start; padding: 10px 14px; border-bottom: 1px solid var(--border); }
.notif-row:last-child { border-bottom: none; }
/* .list-row has no focus style at all — an anchor list you can tab through with
   no visible focus is a keyboard dead end. Inset, so the ring is not clipped by
   the panel's or the card's overflow. */
.notif-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Fixed gutter, occupied in BOTH states. A dot that collapses when read shifts
   every following row 14px the moment one is opened. */
.notif-dot { flex: none; width: 8px; height: 8px; margin-top: 7px; border-radius: 50%; background: transparent; }
.notif-row.is-unread .notif-dot { background: var(--accent); }
.notif-row.is-unread .list-row-subject { font-weight: 600; }

.notif-row-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
.notif-row-actor { color: var(--text-quiet); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --accent-on-tint, NOT --accent-dark: --accent-dark has no dark binding while
   --accent-tint does, which is exactly how .chip-verb came to measure 2.17:1. */
.chip-notif-inbox { background: var(--accent-tint); color: var(--accent-on-tint); flex: none; }
.chip-notif-comment { background: var(--chip-bg); color: var(--chip-text); flex: none; }

/* ---- MS-30 Slice 2 (docs/TICKET-PAGE-REDESIGN.md §2/§3) ---- */

/* Header people row: one row of chip-groups under the ticket header. Chips
   reuse the .req-chip recipe; empty groups render nothing server-side, so a
   simple CR shows a single Requestor chip (the standing constraint). */
/* Sentence case, NOT uppercase. Nested inside the header's People slot these
   sat 6px from an identically-styled uppercase .meta-strip-label, so
   "PEOPLE REPORTER" read as one phrase instead of a key and a group name.
   The row key is now the only uppercase word. */
.people-group-label { font-size: var(--fs-1); color: var(--text-quiet); letter-spacing: 0.02em; }
/* The "+ People" popover reuses .card-menu's disclosure grammar; the panel is
   its own class because it holds tag-add lanes, not menu items. */
.people-pop-lane { display: flex; flex-direction: column; gap: var(--space-2); }

/* Watch eye-toggle ("Watch · N" / "Watching · N") in the header actions. */
.watch-toggle-form { display: inline-flex; margin: 0; }
.watch-toggle { display: inline-flex; align-items: center; gap: var(--space-2); }
.watch-toggle[aria-pressed='true'] { color: var(--accent-text); border-color: var(--accent); }

/* Header attachment droparea: the Description block only — never the whole
   page — becomes a drop target; the dashed overlay is decoration shown on
   drag-over. pointer-events:none so the overlay never steals the drop. */
.header-droparea { position: relative; }
.attach-chip { display: inline-flex; align-items: center; gap: var(--space-2); }
/* Every sibling recipe (.icon-btn, .btn-new, .stepper-jump) declares this ring
   explicitly, so .btn was the one family relying on the UA default. Stated once
   here rather than a fourth time per-recipe. */
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.attach-chip-text { font-size: var(--fs-1); }
/* The create forms' chosen-files list (2026-08-23, ui-framework ruling §3):
   the chip there is a <label> over a hidden multi-file input — no ticket
   exists yet to open a dialog against — and a native file input gives no way
   to un-choose ONE file, so each row carries a .req-chip-x remove. Quiet
   small list; empty renders nothing (the chip with no count already says
   "no files"). */
.attach-chip-filelist { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.attach-chip-filelist li { display: flex; align-items: center; gap: var(--space-3); font-size: var(--fs-2); color: var(--text-quiet); }
/* Same [hidden]-vs-display rule the workspace picker learned the same day. */
.attach-chip-filelist[hidden] { display: none; }

.attach-drop-hint { font-size: var(--fs-1); color: var(--text-quiet); }
.header-droparea-overlay { display: none; position: absolute; inset: 0; z-index: 4; align-items: center; justify-content: center; border: 2px dashed var(--accent); border-radius: var(--radius-md); background: var(--accent-tint); color: var(--accent-on-tint); font-size: var(--fs-3); pointer-events: none; }
.header-droparea.is-dragover .header-droparea-overlay { display: flex; }

/* The "Communication" section label (ADR §1). Same uppercase quiet grammar as
   .meta-strip-label so the page has ONE eyebrow treatment, not two. */
/* ROUND 20 (UX review, 2026-08-07): this is now an <h2>, not a <p> — see the
   three call sites. font-weight is pinned to normal because a bare <h2>
   defaults to bold in every browser, and this class's whole point (quiet,
   uppercase, small-caps-style label) depends on it NOT looking heavier than
   surrounding text; without this line the tag change would be an invisible
   accessibility fix that was also a visible weight regression. */
.feed-section-label { font-size: 11px; font-weight: normal; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-quiet); margin: 0 0 var(--space-3); }

/* The header's People slot: its "People" key beside the chip-groups. Baseline
   alignment so the uppercase label sits on the chips' text line, not their box
   top. `.people-row` already carries its own flex+gap for the groups. */

/* Communication composer (§3): type dropdown + per-type merged input. */
/* scroll-margin-top clears the 84px compact sticky bar. The ✎ Edit pencil
   scrollIntoView()s this card, and because the composer sits at the foot of
   the reading column the bar is always active when that fires — so without
   this it painted over the banner and the type dropdown, the two things the
   click had just changed. Same 84px the stepper's own jump handler uses. */
.cr-composer { margin-top: var(--space-6); scroll-margin-top: 84px; }
/* The wireframe's .composer-top: the type dropdown and its chain caption on ONE
   row. Baseline-aligned so the 11px note sits on the select's text line rather
   than its box top; wraps to a second line on a phone rather than squeezing the
   select. */
/* The Development checklist, INSIDE the feed, belongs to the Development post
   above it — not beside it as a peer. .cr-feed is a flex column with a gap, so
   without this the card sat full-width and chrome-identical to a post, and the
   pairing the sponsor asked for existed in the DOM but not on screen. Pulled up
   into the post's gap and indented, matching the wireframe's .feed .pinned. */
.cr-feed .cr-dev-checklist { margin: calc(-1 * var(--space-4)) 0 0 var(--space-6); }
/* ROUND 9 — the pinned checklist is a BOX too, or round 7 stranded it.
   Once every post around it gained a border, an unboxed checklist sitting in
   the middle of the feed stopped reading as "a different kind of thing" and
   started reading as a rendering fault.
   The wireframe boxes it the same way it boxes a post — `.pinned` is a border,
   a radius, a `.pinned-head` under a border-bottom and a padded `.pinned-body`
   — but fills it with `--wash` where a post takes the page ground. That
   difference is the whole point and is kept: a post is a dated statement on
   `--surface`; this is a live CONTROL whose ticks change after the fact
   (ADR §3c), so it sits on `--bg` and keeps its real <h3> heading and its
   missing type chip. Same anatomy, deliberately not the same object. */
.reading-column .pref-card.cr-dev-checklist {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.cr-dev-checklist > .pref-card-body { padding: 0; }
.cr-dev-checklist .section-heading-row {
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
  margin-bottom: 0;
}
.cr-dev-checklist > .pref-card-body > *:not(.section-heading-row) {
  padding-left: var(--space-6);
  padding-right: var(--space-6);
}
.cr-dev-checklist > .pref-card-body > *:last-child { padding-bottom: var(--space-5); }

.cr-composer-top { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.cr-composer-top .cr-composer-chain { flex: 1 1 16ch; margin: 0; }
/* ===== Round 9 (sponsor 2026-08-08): the composer's type picker is a BUTTON
   ROW on top of the box, not a <select>. A dropdown hides the step's whole
   vocabulary behind a click; the row IS how a reader learns what this phase
   allows. Pill shape (not the app's square buttons) so the row reads as a
   segmented CHOICE rather than a line of separate actions — the one live
   action is the Post/decide button under the box. The single locked chip
   keeps the dashed "not yet" grammar the header's add-affordances use. */
.composer-types { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-4); }
/* A row with no add-buttons is not a row — the rule is unchanged, it is just
   enforced here now instead of by not rendering the element. The element has to
   survive because it is an htmx out-of-band swap target (see
   composerTypeButtons): a target that disappears exactly when it is needed is
   not a target. */
.composer-types:empty { display: none; }
.composer-type {
  font: inherit;
  font-size: 12px;
  line-height: 1.2;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-quiet);
  cursor: pointer;
}
.composer-type:hover { color: var(--text); border-color: var(--text-quiet); }
.composer-type:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.composer-type.is-on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
/* A locked step is a chip PLUS A LOCK, with the reason in the title (sponsor
   2026-08-09: "should it be hint?", and the 2026-08-08 lock rule it follows —
   "just lock icon, hint for more detail"). Before this the full sentence WAS
   the button text, which made the widest control in the row the one nobody can
   press. inline-flex so the glyph sits on the text baseline rather than
   stretching the chip. */
.composer-type.is-locked {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border-style: dashed;
  background: none;
  color: var(--text-quiet);
  cursor: not-allowed;
  opacity: 0.8;
}
/* The decision row — the composer's foot when a verdict is live: a quiet
   caption left, the three real outcomes right, in one flex line. Same
   note-left/actions-right recipe as the rehomed bands (GATE-19). */
.composer-decide-row {
  display: flex;
  /* Baseline rather than centre, so the 11px caption and the 27px buttons put
     their TEXT on one line by construction rather than by coincidence.
     Measured honestly: centring was only 0.5px out here, so this is exactness,
     not a visible fix — the misalignment the sponsor saw was the width below.
     Kept because a caption whose font-size later changes would drift under
     centring and cannot under baseline. */
  align-items: baseline;
  /* The row must SPAN its column. Its parent (.cr-composer-panel) is a flex
     column with align-items:flex-start, so every child shrinks to its content
     — this row was 670px inside a 900px composer. That silently disabled the
     margin-right:auto below, so the "note left, actions right" this recipe
     claims never happened: caption and buttons just huddled at the left edge
     while the Post button they replace sits hard right. */
  align-self: stretch;
  /* Actions to the RIGHT edge, where this composer's plain Post button already
     sits — so the foot does not jump left the moment a record-level act
     appears beside it. justify-content rather than leaning on the caption's
     margin-right:auto, because half these rows (CR's Close, Project's bands)
     have NO caption: stretching them without this left the buttons stranded
     mid-row with 540px of empty space beside them. The caption keeps its auto
     margin, which pushes it left when there IS one. */
  justify-content: flex-end;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.composer-decide-who { font-size: 11px; color: var(--text-quiet); margin-right: auto; }
/* Round 10 (sponsor 2026-08-08: "Lock section [just lock icon] — hint for
   more detail"): a locked communication card shows ONE padlock; the reason
   is its title/aria-label, so hover and assistive tech get the full line
   without the card head carrying a sentence that competes with the byline. */
.lock-chip { display: inline-flex; align-items: center; color: var(--text-quiet); cursor: help; }
.lock-chip:hover { color: var(--text); }
/* Post sits bottom-RIGHT of the composer, as the wireframe's .composer-bottom
   does — the panel is a flex column, so the button just claims the end. */
.cr-composer-panel > button[type='submit'] { align-self: flex-end; }
/* The prerequisite chain, stated once under the type dropdown. Advisory copy —
   the dropdown's locked row is what actually enforces the order. */
.cr-composer-chain { font-size: var(--fs-1); color: var(--text-quiet); margin: var(--space-2) 0 0; }
.cr-composer-panel { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); margin-top: var(--space-4); }
/* Setting `display` on a class whose members carry the `hidden` ATTRIBUTE
   silently defeats it: an author rule beats the UA's `[hidden]{display:none}`
   regardless of specificity. Without this the composer rendered EVERY panel at
   once — the type dropdown said "Comment" while a Requirement textarea sat
   above it (caught driving the rebuilt app, MS-30 P4 round 1). Same shape as
   .bulk-bar[hidden] above, which exists for exactly this reason. */
.cr-composer-panel[hidden] { display: none; }
/* The merged input: the textarea IS the drop target; the Attach pill floats
   bottom-right 8px inset, so padding-right must clear it. */
.cr-composer-field { position: relative; width: 100%; }
.cr-composer-textarea { width: 100%; resize: none; max-height: 240px; min-height: 72px; padding: 10px 110px 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); color: var(--text); font-size: var(--fs-3); }
.cr-composer-textarea.is-dragover { border: 2px dashed var(--accent); background: var(--accent-tint); }
/* TOP right, matching every communication card's own attach chip (sponsor
   2026-08-12). Only the POSITION is set here — the look comes from
   .btn-quiet.btn-small.attach-chip, so the composer's trigger and the cards'
   cannot drift apart. Bottom-right was where a typist's last line lands; the
   textarea's right padding still clears it on the first line, which is the
   only one it can now reach. */
.cr-composer-attach-pill { position: absolute; right: 8px; top: 8px; }
/* `.cr-composer-uat-evidence` (the required-evidence UAT variant: a text-first
   label and an amber textarea border) was deleted in MS-30 P0.1, 2026-08-08.
   The composer's UAT chip now posts a ROUND, and evidence is the UAT_SIGNOFF
   Approval gate's precondition, not a round's — so there is no requirement for
   the amber to announce, and no emitter left for these two rules. */
/* The edit-mode banner ("Editing Requirement — replaces the current text…"). */
.cr-composer-banner { background: var(--amber-tint); color: var(--amber); border: 1px solid var(--amber-border); border-radius: var(--radius-sm); padding: var(--space-3) var(--space-4); font-size: var(--fs-2); margin-bottom: var(--space-4); }

/* Section-side singleton controls: the ✎ Edit row and the "edited" marker —
   a control opening provenance (title + activity-log link), never a label. */
.composer-edit-row { display: flex; align-items: center; gap: var(--space-4); margin-top: var(--space-3); }
/* ...but NOT when it rides the post head. The row was written for the foot of
   a body, where a top margin separates it from the text above; inside
   .feed-post-aside it is one item in an inline group and that margin only
   pushes it off the head's baseline. */
.feed-post-aside .composer-edit-row { margin-top: 0; gap: var(--space-2); }
.edited-marker { font-size: var(--fs-1); color: var(--text-quiet); text-decoration: underline dotted; }

/* ---- MS-30 P3 (docs/TICKET-PAGE-REDESIGN.md §1/§3): the Communication feed
   as the PRIMARY read surface ---- */

/* The feed itself: one chronological run of typed posts where P1 left a stack
   of headed sections. Only rhythm lives here — every post keeps its own box
   recipe (.pref-card for the singleton posts, .deployment-submission-row for
   the repeatable Transport posts), so this adds no fourth card vocabulary. */
.cr-feed { display: flex; flex-direction: column; gap: var(--space-5); }
/* Round 11 (sponsor 2026-08-08: "if card is editable to the user, can border
   become different color"): a card THIS viewer may write carries an accent
   left edge and a tinted border. It answers, at a glance and before any
   hover, the question the page otherwise makes you discover by clicking:
   which of these is mine to fill in. Keyed off the server's own `edit` slot
   (communicationCard adds .is-editable only when it renders an editor), so
   the colour can never promise a write the server would refuse — the rule
   the lock chip follows from the other side. */
/* Round 11's editable marker LIVES BELOW, next to the rule that was silently
   beating it. Kept as a pointer rather than a second definition: two rules for
   one look is how the first one stops being read. */

/* A post's header line, replacing the section heading row: TYPE TAG, then
   whatever authorship the data actually stores, then the section's own
   trailing slot (attachment trigger / status pill / amend controls). */
.feed-post-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-3); }
/* The type tag is the post's HEADING (an <h3>) wearing the existing `.chip`
   recipe — never a fourth chip. Only the deltas live here: the uppercase
   treatment that distinguishes a post type from a status/workspace chip, and
   the heading resets (margin, font-size) that keep it visually identical to
   the <span> it replaced. */
/* ROUND 2 — the type tag is the wireframe's SOLID inverted badge
   (`color:var(--bg); background:var(--ink)`), not uppercase text.
   `.chip`'s base recipe carries only padding/radius/font — the fill always
   comes from a modifier (.chip-status-*, .chip-workspace, .chip-priority-*),
   and this had none, so the one element that says WHAT KIND OF POST this is
   rendered as bare text in the body colour. In a feed of five posts the type
   is the thing you scan; it has to be the highest-contrast mark on the head
   line, which is exactly why the drawing inverts it.
   --text/--surface mirror the wireframe's --ink/--bg and invert correctly in
   dark mode of their own accord. Squarer than the pill base: the drawing's
   type-tag is a 4px-radius rectangle, deliberately a different SHAPE from the
   status pills it shares the line with. */
.feed-post-type {
  margin: 0;
  font-size: var(--fs-1);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--text);
  color: var(--surface);
  border-radius: var(--radius-sm);
}
.feed-post-meta { font-size: var(--fs-1); color: var(--text-quiet); }
/* ROUND 3 — the wireframe's `.who b`: the NAME lifts out of the quiet byline,
   the role and the timestamp stay quiet behind it. One line, one person, two
   facts about the post — and the person is what a reader scans a feed for. */
.feed-post-author { color: var(--text-soft); font-weight: 600; }
/* The head's trailing slot (attach trigger / status pill + amend controls),
   pushed to the far end. It is a WRAPPER, not `:last-child`: attachSection's
   trigger is two siblings (the button AND its closed <dialog>), so a
   :last-child rule landed on the dialog — a box-less element — and the
   paperclip never moved. */
/* flex-wrap because this slot now holds TWO groups — the edit row (✎ Edit /
   🔒 lock / "edited") and the attach chip — not just the attach chip. On a
   phone the head's own wrap puts the aside on its own line; without this the
   two groups then overflow that line rather than stacking. margin-left:auto
   still right-aligns it whenever it shares a line with the byline. */
.feed-post-aside { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; justify-content: flex-end; }
/* Posts that carry their own box already (.deployment-submission-row) need the
   post padding the .pref-card ones inherit; this is scoped so it cannot leak
   into the pref-card posts, which already have theirs. */
.feed-post:not(.pref-card) { padding: var(--space-5); }
/* ROUND 4 — ADR §3c placement 1, drawn the way the wireframe draws it.
   `.decision` is INSIDE `.post` there, under a border-top and on the same wash
   as `.post-top` — the head band's mirror at the foot. This used to be an
   indented sibling BELOW the card, which only says "related to the thing
   above" and leaves the reader to infer ownership from a margin. Inside the
   border there is nothing to infer.
   A direct child of the card, never of .pref-card-body: it must span edge to
   edge, and the body carries the inset that would stop it. Bottom corners
   match the head's top ones so the card reads as one object. */
.feed-post-decision {
  border-top: 1px solid var(--border);
  background: var(--bg);
  padding: var(--space-4) var(--space-5);
  border-radius: 0 0 calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px);
}
/* ROUND 5 — the band's own contents, and the box-in-a-box round 4 created.
   decisionCard renders `.approval-banner-note`, which carries an amber fill, a
   border, its own padding and `margin-bottom: 20px`. That was right when the
   card floated free in the reading column; nested in a band it draws a second
   box inside the first and leaves 20px of dead space at the card's foot.
   The BAND is now the box, so the banner inside it sheds its chrome.

   The amber is dropped deliberately, not lost: this text says WHO MAY DECIDE
   ("Waiting on UAT sign-off — …"), which is the wireframe's `.who-decides` —
   quiet informational copy, not a warning. Amber in this app means attention
   (`.banner-warn`, the DM's missing-evidence note); spending it on a standing
   fact about every open gate is what makes a warning colour stop working.
   Scoped to the band, so the free-standing banner elsewhere is untouched.

   Layout is the drawing's `.decision`: who-decides left, controls hard right,
   space-between, wrapping on a narrow screen rather than crushing the note. */
/* The Transport Request card's own band controls — Submit for approval, and
   Delete at the far END (sponsor 2026-08-13: delete while unapproved). One row,
   because they are alternatives at the same moment, not a sequence; the
   destructive one is pushed away from the primary by margin-left:auto rather
   than by a separator, which is how the rest of this app separates a "do it"
   from an "undo it".

   The blocking sentence ("Add a backout plan first.") sits between them, beside
   the disabled button it explains — a reason placed anywhere else is a reason
   the reader has to go looking for. */
.tp-band-controls { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.tp-band-controls form { margin: 0; }
/* AFTER the `margin: 0` above, and scoped, or source order wins and the
   destructive control sits shoulder to shoulder with the primary one. */
.tp-band-controls .tp-band-end { margin-left: auto; }
.feed-post-decision .approval-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.feed-post-decision .approval-banner-note {
  background: transparent;
  border: none;
  color: var(--text-quiet);
  padding: 0;
  margin-bottom: 0;
}
.feed-post-decision .approval-banner p { margin: 0; font-size: var(--fs-2); }
/* The decision form is the row's right-hand item, so it stops stretching to
   full width and sizes to its own controls. */
.feed-post-decision .decision-card { flex: 1 1 auto; justify-content: flex-end; }
/* The same card when NO post owns it — a Project gate whose stage has not been
   written yet. No left indent: the indent means "belongs to the post above",
   and here there is none. */
.project-decision-standalone { margin: var(--space-3) 0 0; }

/* A COMMENT in the feed is NOT a card (sponsor ruling 2026-08-06). The stage
   notes are the record — bordered, tagged, amendable; a comment is
   conversation around that record, so it drops the border and the type chip
   and indents slightly, letting the project's spine stay legible through the
   chatter. It keeps its author and time: who said it and when is the whole
   value of a comment. */
.feed-comment { padding: 0 0 0 var(--space-6); border-left: 2px solid var(--border); }

/* THE communication card (sponsor 2026-08-06: one recipe, reusable across
   every post type, with inline edit). Chrome comes from .pref-card/.feed-post;
   these rules are only what the INLINE EDITOR needs.

   A hidden checkbox drives it, NOT a <details>: the ✎ pencil belongs in the
   head (the wireframe's .post-actions) while the editor needs the card's full
   width below the body, and a <details> traps its content in whatever box the
   <summary> sits in — driving it produced a textarea squeezed into the head's
   inline-flex aside. The checkbox sits at card level, the <label> in the head,
   and these sibling selectors do the rest. No script, so the state survives a
   full page swap; the input stays focusable, so it is not mouse-only. */
/* ROUND 1 — the wireframe's post is TWO BANDS, not one flat box.
   Its `.post-top` carries `background:var(--wash)` and its own padding above a
   separately-padded `.post-body`; the build had head and body sharing one
   `.pref-card-body`, so the type tag, the byline and the prose all sat on one
   undifferentiated field. On a feed of five posts that is the difference
   between scanning the spine and reading everything.

   The band is `--bg` — the page wash, one step off `--surface` in BOTH themes
   (gray-50 under white; #10141b under #171c25). No new token.

   Corners are rounded explicitly rather than clipped with `overflow:hidden` on
   the card: attachSection's trigger puts a real <dialog> inside this head, and
   a clipping ancestor is how that stops being openable. `-1px` keeps the fill
   inside the card's own border instead of riding on top of it. */
/* ROUND 7 — a POST is a box again, and every post is banded, not just the two
   already on the shared card.

   `.reading-column .pref-card` strips background, border, shadow and radius:
   the 2026-07-21 "can we just show the content as medium.com?" rule, which is
   right for what it was written for — a flowing article of sections separated
   by whitespace and headings. MS-30 turned those sections into POSTS in a
   chat-like feed, and the wireframe boxes every one of them
   (`.post{border:1px solid var(--line-2); border-radius:8px}`). The old rule
   was silently overruling the new design of record, so rounds 1 and 4 were
   painting bands onto cards with no visible edges. Re-boxed for feed posts
   ONLY — every other reading-column card still flows.

   And the banding moves off `.cc-card` onto `.feed-post.pref-card`, because
   CR's Description, Proposal and UAT sign-off are still editableSection posts.
   Scoped to .cc-card, round 1 reached Requirement and Development and left the
   other three flat — five posts in one feed wearing two different card
   designs, which is worse than the gap it was fixing. The recipes stay
   separate (that conversion is its own change); the LOOK does not. */
.reading-column .pref-card.feed-post {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
/* ROUND 11'S EDITABLE MARKER (sponsor 2026-08-08: "if card is editable to the
   user, can border become different color") — a card THIS viewer may write
   carries an accent left edge and a tinted border. It answers, at a glance and
   before any hover, the question the page otherwise makes you discover by
   clicking: which of these is mine to fill in. Keyed off the server's own
   marker, so the colour can never promise a write the server would refuse.

   IT HAD NOT RENDERED SINCE ROUND 7 (found 2026-08-13 by reading
   getComputedStyle in a browser rather than the class list in the markup).
   The rule sat 180 lines above `.reading-column .pref-card.feed-post`, which is
   BOTH more specific (0,3,0 against 0,2,0) AND later, and which sets the
   `border` shorthand — so it reset all four borders, accent included, on every
   card in the reading column of all three record types. The class was on the
   element the whole time; that is exactly why nothing caught it. A test that
   greps the markup for `is-editable` passes against a dead feature.

   So: scoped to the reading column to match that rule's specificity, placed
   AFTER it to win the tie, and written as longhand `border-left` plus
   `border-color` so a future shorthand cannot quietly wipe it again. */
.reading-column .cc-card.is-editable,
.cc-card.is-editable {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  border-left: 3px solid var(--accent);
}
.feed-post.pref-card > .pref-card-body { padding: 0; }
/* The band spans the card edge to edge; everything else in the body keeps the
   reading inset. `:last-child` closes the card's foot — .pref-card-view-content
   sets `padding-bottom: 0` of its own, so without this the prose would sit on
   the border. */
.feed-post.pref-card > .pref-card-body > *:not(.feed-post-head) {
  padding-left: var(--space-6);
  padding-right: var(--space-6);
}
.feed-post.pref-card > .pref-card-body > *:last-child { padding-bottom: var(--space-5); }
.feed-post.pref-card .feed-post-head {
  margin-bottom: 0;
  padding: var(--space-4) var(--space-5);
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
}
.cc-card .cc-view { padding: var(--space-5) var(--space-6); }
/* THE PENCIL IS GONE (sponsor 2026-08-12: "in change request, each card. do we
   need edit button? pls refer to incident. can follow the same edit method?").
   The hidden checkbox, its label, and the rules that swapped a reading view
   for an editor twin are deleted rather than left unreachable — the card now
   has ONE copy of its text, which displays it and becomes editable in place,
   exactly as Root Cause and Solution already did.
   The form is a plain block INSIDE .cc-view, so it inherits that padding
   instead of carrying its own; it no longer stands in for the body, it IS the
   body. */
.cc-editor-form--inline { display: flex; flex-direction: column; gap: var(--space-3); }
.cc-editor-actions { display: flex; justify-content: flex-end; }
/* The CARD-LEVEL row (cardDirtyFooter) — for cards whose editor lives in the
   body rather than the `edit:` slot, i.e. Root Cause and Solution. Sponsor
   2026-08-12: "should it be added at the footer of card like header card?"
   Two things distinguish it from the in-form row above:
   1. It is a direct child of .pref-card-body, which no longer carries padding
      (that moved to .cc-view), so it must supply its own — otherwise the
      buttons sit flush against the card border.
   2. It is ALWAYS in the DOM, so the row itself hides when clean. Leaving it
      displayed with hidden children still spends .pref-card-body's gap and
      this padding, i.e. a permanent empty strip at the foot of every card. */
.cc-card-footer {
  display: none;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6) var(--space-5);
}
.cc-card:has(.rt-dirty) .cc-card-footer,
.cc-card:has(.card-field-dirty) .cc-card-footer { display: flex; }
/* Pushes the two acts right and the note left, exactly as the Detail card
   reads. */
.cc-card-footer .ticket-header-dirty { margin-right: auto; }
/* The label is the visible control, so it carries the focus ring the
   visually-hidden checkbox cannot show. */
.feed-comment-meta { font-size: var(--fs-1); color: var(--text-quiet); margin: 0 0 var(--space-2); }

/* ADR §3c placement 3: the aggregate-gate strip (DEPLOYMENT_APPROVAL / CLOSE)
   above the feed — decisions no single post owns, with the server-loaded
   submission counts that frame them. */
/* SPONSOR 2026-08-07 ("do we still need Activity History showing below?") —
   kept, collapsed. ADR §1 logged the below-the-composer placement without a
   ruling and named "collapsed by default" as one of its two reopenings; this
   is that. The log still carries what only it carries (status changes,
   assignments, gate decisions) and is still where every post's "edited" marker
   opens provenance — but the FEED is the chronology a reader came for, so the
   page ends on the composer instead of a wall of log rows.
   A native <details>, no script,
   state survives a body swap. */
.ticket-activity-details { border-top: 1px solid var(--border); margin-top: var(--space-6); padding-top: var(--space-5); }
.ticket-activity-summary {
  cursor: pointer;
  font-size: var(--fs-2);
  font-weight: 600;
  color: var(--text-quiet);
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}
.ticket-activity-summary::-webkit-details-marker { display: none; }
/* The disclosure needs its own affordance once the native marker is gone. */
.ticket-activity-summary::before { content: "▸"; font-size: 10px; }
.ticket-activity-details[open] > .ticket-activity-summary::before { content: "▾"; }
.ticket-activity-summary:hover { color: var(--text); }

/* The record-level decision band, at the END of the reading column since the
   2026-08-09 sweep ("Close change request should be at the end"). Two fixes
   from the sponsor's follow-up ("pls fix them"), both composition not logic:

   THE FULL-WIDTH "Transport failed" BAR was a flex accident, not a design: a
   column flex container stretches its children by default, so the one bare
   .btn in the strip (the rework trigger — decisionCard's own form manages its
   row) inflated to the column's width and read as a banner pretending to be a
   button. align-self on the child, not align-items on the strip, because the
   counts line and the decision card SHOULD span.

   THE HAIRLINE + breathing room: with the strip now adjacent to the composer,
   nothing marked where the feed's history ends and the record's decision
   begins — counts, Close, Transport failed and the composer chips read as one
   stack. Same border-top idiom every other section boundary in this file uses. */
.cr-gate-strip {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Tightened from --space-6 (2026-08-14): the band carries three purposeful
     lines now — a heading, the counts that justify the decision, and the
     decision itself — where it used to carry one orphaned paragraph and needed
     the air to look deliberate. */
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
  margin-bottom: var(--space-5);
}
/* A HAIRLINE, NOT A BOX, deliberately: the amber `waitingOnNote` banner sits
   directly above this and IS a box, so giving this one a border too would read
   as two stacked alerts — and a decision you are being invited to make is not a
   warning. */
/* The heading is the card type chip's recipe, reused (as
   `.ticket-header-summary-tag` already reuses it), so the band announces itself
   in the same vocabulary as the posts above it. Left-aligned rather than
   stretched: a chip that spans 900px stops reading as a chip. */
.cr-gate-strip-heading { align-self: flex-start; }
/* ONE DECISION, ONE ROW — the approve half and the reject half arrive from two
   different builders (see aggregateGateStrip) and used to stack as two
   full-width boxes. `flex-wrap` because on a narrow column the note input
   earns its own line before the buttons do. */
.cr-gate-decision-row { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.cr-gate-strip-counts { margin: 0; font-size: var(--fs-2); color: var(--text-quiet); }

/* AI Assistant panel (2026-08-16, ui-framework ruling — see
   docs/DESIGN-SYSTEM.md's own entry for the full rationale). A FIXED
   overlay, never a docked column: MS-30 P4 retired the two-column rail
   specifically because a fixed-width sibling column turns the reading
   column's own width into an unplanned residual, and `.ai-panel` sitting
   in flow beside `.ticket-reading-single` would be that identical defect
   under a new name. `position: fixed` takes it out of document flow
   entirely, so the reading column's own 900px cap and centering are never
   affected by whether the panel is open. Width (360px) matches this app's
   existing narrow-surface idiom (`.action-dialog`'s own narrower variants).
   Below 1280px — the width `.ai-panel` needs to dock without squeezing the
   capped reading column (900px + 360px + gutters) — it drops the docked
   right-edge shape for a full-width bottom sheet, reusing `.action-dialog`'s
   own scrim/backdrop grammar for that state: one component, a responsive
   shape change, not two components wearing one name. */
.ai-panel[hidden] { display: none; }
.ai-panel-scrim { display: none; }
.ai-panel-surface {
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  height: 100dvh;
  width: 360px;
  max-width: 100vw;
  background: var(--surface);
  color: var(--text);
  border-left: 1px solid var(--border);
  box-shadow: -4px 0 12px rgba(0, 0, 0, 0.12);
  z-index: 60;
  display: flex;
  flex-direction: column;
}
@media (max-width: 1279px) {
  .ai-panel-scrim { display: block; position: fixed; inset: 0; background: var(--backdrop); z-index: 59; }
  .ai-panel-surface {
    top: auto;
    right: 0;
    left: 0;
    bottom: 0;
    height: min(80vh, 640px);
    width: 100%;
    border-left: none;
    border-top: 1px solid var(--border);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }
}
.ai-panel-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); flex: none; }
.ai-panel-head-badge {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--accent);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.ai-panel-head-titles { flex: 1 1 auto; min-width: 0; }
.ai-panel-title { font-weight: 600; font-size: var(--fs-3); }
.ai-panel-sub { font-size: var(--fs-1); color: var(--text-quiet); }
.ai-panel-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 16px; display: flex; flex-direction: column; }
/* Same uppercase-eyebrow treatment as .feed-section-label — the
   "Communication" eyebrow precedent this page family already uses, not a
   second typographic idea for one label (plain CSS here, no build step to
   compose rules with, so the values are repeated rather than inherited). */
.ai-panel-section-label { text-transform: uppercase; letter-spacing: 0.04em; font-size: var(--fs-1); color: var(--text-quiet); font-weight: 600; margin: 14px 0 6px; display: flex; align-items: center; gap: 6px; }
.ai-panel-section-label:first-child { margin-top: 0; }
.ai-panel-section-num {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--border);
  font-size: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-quiet);
}
/* Typography matches .rt-editor's own prose rules so the preview reads as
   "this is literally the field," not a distinct component. */
.ai-panel-message-preview { max-height: 30vh; overflow-y: auto; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--fs-3); background: var(--bg); }
.ai-panel-message-preview p { margin: 0 0 8px; }
.ai-panel-message-preview p:last-child { margin-bottom: 0; }
.ai-panel-live-caption { font-size: var(--fs-1); color: var(--text-quiet); margin: 6px 0 0; }
.ai-panel-chat { flex: 1 1 auto; min-height: 120px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 4px 0; }
.ai-panel-chat-empty { text-align: center; margin: 20px 0; }
.ai-panel-chat-system { text-align: left; }
.ai-panel-suggestion-text { margin: 0; white-space: pre-wrap; }
.ai-panel-bubble-actions { margin-top: 8px; }
.ai-panel-inserted-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-1);
  color: var(--green);
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 1px 10px;
  margin-top: 8px;
}
.ai-panel-loading .chat-message-bubble { opacity: 0.55; }
.ai-panel-input-row { display: flex; gap: 8px; align-items: flex-end; border-top: 1px solid var(--border); padding-top: 10px; margin-top: 10px; flex: none; }
.ai-panel-input-row textarea { flex: 1 1 auto; min-height: 40px; max-height: 120px; resize: vertical; font: inherit; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
.ai-panel-send {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex: none;
}
.ai-panel-foot { display: flex; align-items: flex-start; gap: 6px; padding: 10px 16px 14px; font-size: var(--fs-1); color: var(--text-quiet); border-top: 1px solid var(--border); flex: none; }
.ai-panel-foot svg { flex: none; margin-top: 2px; }
