/* screens.css — part 3 of 6 of the app stylesheet.
   Holds: the security indicator, its popover and the Detections screen, planning,
   iterations, SOC 2, notifications, sign-in.

   ORDER MATTERS. These files were cut out of a single app.css at line boundaries with the
   sequence preserved, so concatenating them in the order index.html links them reproduces the
   original byte for byte — which is what makes the cascade provably unchanged. Equal-specificity
   rules still resolve by source order, so reordering the <link> tags can change rendering.

   The names describe what ended up in each part, not a design: the original file interleaved
   the top bar, layout and per-feature rules, and moving rules to where they belong is a separate
   pass that this split exists to make possible. Palette is in theme.css, linked first. */
    /* The Company Links button in the header. */
    .cl-indicator { position: relative; }
    /* The Company Links popover moved onto `.ui-popover` and `.ui-disclosure` in 7.260.0; its eleven
       private rules left with it. The button that opens it (`.cl-btn`) is still here. */
    /* Project/Milestone state picker in the card panel. Explicit background/color — a bare select
       renders white and ignores the dark theme (the same trap the modal inputs hit in 4.4.0). */
    .cd-state {
      font: inherit; font-size: 13px; padding: 3px 8px; border-radius: 999px;
      background: var(--panel-2); color: var(--text); border: 1px solid var(--line); cursor: pointer;
    }
    .cd-state:hover { border-color: var(--muted); }
    /* The Client dropdown (6.93.0) borrows `.cd-state`'s shape — same control, same panel, and the
       explicit background/color it carries for dark mode applies for the same reason. Only the width
       cap and the saving message are its own; a long client name shouldn't stretch the details grid. */
    .cd-client { max-width: 260px; }
    /* Project Planning board. Columns scroll horizontally on narrow screens rather than squeezing to
       unreadable widths (the same overflow lesson as the main layout). */
    /* Backlog review */
    .bl-note { font-size: 13px; color: var(--muted); margin: 0 0 12px; line-height: 1.5; }
    .bl-note strong { color: var(--text); }
    .bl-iter { white-space: nowrap; }
    .bl-age { color: var(--warn); font-size: 12px; font-variant-numeric: tabular-nums; margin-left: 6px; }
    .bl-orphan { color: var(--err); font-size: 12px; margin-left: 6px; }
    .bl-act { text-align: right; white-space: nowrap; }
    .bl-replan-row > td { background: var(--panel-2); }
    /* Toggled via the hidden attribute, so the row's own display must yield to it (§10 gotcha). */
    .bl-replan-row[hidden] { display: none; }
    /* The form is one cell spanning the whole table, so on a phone it was as wide as the table scrolled under it:
       609px in a 358px scroller, with "Why did it slip?" and Move and record off to the right (7.447.0). It is
       pinned to the scroller's left edge and capped at the scroller's width, so it wraps inside what is on screen
       and stays there while the table scrolls under it. The 8px is the cell's own padding, kept on both sides.
       `cqi` reads the scroller's width, which is why the scroller is a size container. */
    .ui-table-scroll:has(.bl-replan) { container-type: inline-size; }
    .bl-replan { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; padding: 4px 0;
      position: sticky; left: 8px; max-width: calc(100cqi - 16px); box-sizing: border-box; }
    /* Iteration management */
    .it-note { font-size: 13px; color: var(--text); margin: 0 0 14px; line-height: 1.5; }
    .it-hint { display: block; color: var(--muted); font-size: 12px; margin-top: 3px; }
    .it-undated { color: var(--warn); }
    /* The table is `.ui-table` in a flush `.ui-panel` since 7.454.0, like the planning and backlog tables beside it.
       What stays is what that table has and the component does not: each row is one line, so on a phone the table
       scrolls sideways instead of squeezing a sprint's dates and inputs onto three lines; the text sits level with
       the date inputs and Save in the same row; and the current sprint's row is shaded. */
    table.it td { vertical-align: middle; white-space: nowrap; }
    table.it tr.it-is-cur > td { background: var(--panel-2); }
    /* The new-iteration fields stay a top-aligned wrapping row. `.ui-toolbar--fields` aligns to the bottom, and an
       input and a select are different heights, so their labels would sit out of line. */
    .it-new-grid { display: flex; flex-wrap: wrap; gap: 12px; }
    .pl-f { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; }
    .pl-board { display: flex; gap: 14px; align-items: flex-start; overflow-x: auto; padding-bottom: 8px; }
    .pl-col { flex: 1 1 240px; min-width: 240px; background: var(--panel-2); border: 1px solid var(--line);
      border-radius: 12px; padding: 10px; }
    .pl-colhead { font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: .04em;
      color: var(--muted); display: flex; justify-content: space-between; margin-bottom: 8px; }
    .pl-count { background: var(--line); color: var(--text); border-radius: 999px; padding: 0 8px; font-weight: 600; }
    .pl-cards { display: flex; flex-direction: column; gap: 8px; }
    /* The card is a `.ui-card--action` since 7.456.0; the hours keep the right end of its meta row. */
    .pl-hrs { margin-left: auto; }
    /* `.pl-table` was this app's second table look, shared by five tables across three screens. It is
       now only the one thing the component layer has no opinion about: the space above the table.
       Width, collapse, type size, header treatment, row separators and cell padding all come from
       `.ui-table` in components.css.

       THE ROW POINTER AND THE ROW HOVER LEFT WITH IT, AND THAT IS THE POINT. They were declared on the
       base, so all five tables advertised a click — and two of them have no row handler at all. The
       SOC 2 controls table renders a bare `<tr>` with no `data-id`, and the detections table binds only
       its checkboxes, its sort buttons and its two row buttons. Both showed a pointer cursor and a
       hover fill on rows that do nothing. `.ui-table--rows` is the opt-in, and a table now carries it
       only if its rows open something. */
    .pl-table { margin-top: 22px; }
    .pl-link { color: var(--link); }
    .pl-notes { color: var(--muted); min-width: 120px; }
    /* Status shrinks to its pill instead of being squeezed by the Task column; the pill itself
       has no left margin here because it is the whole cell. */
    .pl-table th:first-child, .pl-table td:first-child { width: 1%; white-space: nowrap; }
    .pl-table td:first-child .pill { margin-left: 0; }
    /* SOC 2 audit discovery. Reuses the planning table shell; only the status/evidence bits differ. */
    /* The milestone and its folder path under the generate bar: a 13px meta line, which no component sets
       (`.ui-toolbar` inherits 15px). Kept private for that reason, like `.s2-summary` and `.s2-span`. */
    .s2-head { display: flex; flex-wrap: wrap; gap: 6px 20px; justify-content: space-between;
      align-items: baseline; margin-bottom: 6px; font-size: 13px; }
    .s2-summary { color: var(--muted); font-size: 13px; }
    /* The calendar span of the selected quarter — the only thing on the bar that distinguishes a fiscal
       Q1 from a calendar one, so it reads as a value beside the picker, not as a footnote. */
    .s2-span { color: var(--muted); font-size: 13px; white-space: nowrap; }
    .s2-evilist { margin: 0; padding-left: 16px; color: var(--muted); }
    .s2-evilist li { margin: 0 0 2px; }
    /* Folder paths are long and shouldn't force the table wide — wrap them mid-path instead.
       The cell drops its own padding so the anchor inside can supply it and fill the whole box
       (.pl-table td out-specifies a bare class, hence the two-class selector rather than !important). */
    /* The cell shows the folder's own name since 7.422.0, so it wraps at spaces like any text. `anywhere` let
       the column shrink until the full path broke mid-word on every row. */
    .s2-folder { max-width: 280px; }
    .s2-table td.s2-folder { padding: 0; }
    /* The anchor fills the cell so the whole box is clickable, not just the text. */
    .s2-folderlink { display: block; padding: 8px 10px; color: var(--link); text-decoration: none;
      border-radius: 6px; }
    .s2-folderlink:hover { background: var(--panel-2); text-decoration: underline; }
    .s2-folderlink.s2-inline { display: inline; padding: 0; border-radius: 0; }
    .s2-ext { font-size: 11px; opacity: .75; }
    .s2-nofolder { display: block; padding: 8px 10px; cursor: help; }
    /* Only the space around a note; the note itself is `.ui-callout` since 7.418.0. */
    .s2-note { margin: 12px 0; }
    /* Open audits: one collapsible block per quarter, open ones expanded. */
    .s2-audit { border: 1px solid var(--line); border-radius: 12px; padding: 8px 12px; margin-bottom: 8px;
      background: var(--panel-2); }
    .s2-audit > summary { cursor: pointer; display: flex; align-items: center; gap: 10px;
      flex-wrap: wrap; font-size: 14px; }
    .s2-audit > summary::-webkit-details-marker { display: none; }
    .s2-audit > summary::before { content: '\25b8'; font-size: 11px; color: var(--muted); }
    .s2-audit[open] > summary::before { content: '\25be'; }
    .s2-tasktable { margin-top: 8px; }
    /* A `.ui-btn` like every other button since 7.455.0. The smaller size this rule asked for never applied (`.btn-sm`
       outranked it), so it keeps only the push to the summary's end. */
    .s2-del-audit { margin-left: auto; }
    .s2-rowdone { opacity: .55; }
    /* Catalog editor */
    /* The Save row is a `.ui-btn-row` since 7.435.0; this keeps the rule and the space that set it apart from
       the control list above it, which no component draws. */
    .s2-cat-actions { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
    /* The raw JSON box: the whole width, in a monospace face. `base.css` paints it like every other control. */
    .s2-json { width: 100%; box-sizing: border-box; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
      font-size: 12px; }
    .s2-ctl { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 6px; background: var(--panel-2); }
    .s2-ctl > summary { cursor: pointer; padding: 8px 12px; display: flex; align-items: center;
      gap: 8px; flex-wrap: wrap; font-size: 13px; }
    .s2-ctl > summary::-webkit-details-marker { display: none; }
    .s2-ctl-n { color: var(--muted); font-variant-numeric: tabular-nums; min-width: 18px; }
    /* The control's own id, on the plan table beside its title. `tabular-nums` so a column of 004/044/112
       lines up, and `white-space: nowrap` because "Pop 17" is one token and wrapping it reads as two. */
    .s2-ref { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap;
      margin-right: 2px; }
    /* Both places a task number appears open the card, so both have to look like they do. The plan
       table's number was a plain span with no handler at all, and neither carried a pointer — which is
       why the one that was not wired is the one somebody clicked. */
    .s2-opencard { cursor: pointer; }
    .s2-opencard:hover, .s2-opencard:focus-visible { text-decoration: underline; }
    /* Naming a piece of evidence: the control id and period sit before the editable part and the file
       type after it, both muted, so what somebody may change is obvious without a second sentence. */
    .s2name-row { display: flex; align-items: center; gap: 4px; }
    .s2name-row input { flex: 1 1 auto; min-width: 0; }
    .s2name-prefix, .s2name-ext { color: var(--muted); white-space: nowrap;
      font-variant-numeric: tabular-nums; }
    .s2-ctl-body { padding: 4px 12px 14px; }
    .btn-sm.danger { color: var(--err); border-color: var(--err); }
    .btn-sm.danger:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
    /* Header notifications bell */
    .notif-indicator { position: relative; }
    /* The bell is `.ui-icon-btn` (7.397.0); its count is pinned to the button's corner. */
    #notif-bell > .ui-badge { position: absolute; top: -5px; right: -5px; }
    /* The row is a `.ui-popover-item`; this is what a <button> needs on top to look like one. */
    .notif-item { width: 100%; text-align: left; cursor: pointer; font-family: inherit; }
    .notif-item.unread { border-left: 3px solid var(--brand-bright); background: var(--panel-2); }
    /* Locally-logged problems. The left edge keeps its severity color whether read or not — unlike a
       server notification, the point of one of these is that something broke, not that it is new. */
    .notif-item.local.warn { border-left: 3px solid var(--warn, #b5790a); }
    .notif-item.local.error { border-left: 3px solid var(--err, #c0392b); }
    /* A raw rejection body is reference text, not the headline: muted and monospaced so a long JSON
       payload wraps predictably and doesn't out-shout the title saying what actually failed. */
    .notif-item.local .notif-body {
      white-space: pre-wrap; word-break: break-word; color: var(--muted);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px;
      max-height: 8.5em; overflow: auto;
    }
    .notif-body { font-size: 12.5px; line-height: 1.4; }
    /* A row and its Dismiss control side by side (7.229.0). The control itself is `.ui-btn`; only the
       pairing is local, because `.notif-item` is full-width by design and would push it to a new line. */
    .notif-row { display: flex; align-items: flex-start; gap: 4px; }
    .notif-row > .notif-item { flex: 1 1 auto; width: auto; min-width: 0; }
    /* The theme toggle and Log out are `.ui-nav__item` in the rail's foot since 7.473.0. The toggle's words and its
       icon's size are its own, because theme.js writes only the icon; Log out stays hidden until shell.js knows a
       session exists, and shows it as the item's row. */
    .theme-toggle::after { content: 'Toggle theme'; }
    .theme-toggle svg { width: 18px; height: 18px; flex: none; }
    .signout { display: none; }
    .signin-wrap { min-height: calc(100vh - 120px); display: grid; place-items: center; }
    .signin {
      max-width: 420px; margin: 0 auto; text-align: center;
      background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
      padding: 34px 28px; box-shadow: var(--shadow);
    }
    .signin-icon { width: 64px; height: 64px; border-radius: 50%; }
    .signin h2 { font-family: 'Quicksand', system-ui, sans-serif; color: var(--brand); margin: 14px 0 6px; }
    .signin p { color: var(--muted); margin: 0 0 22px; }
    .signin-btn {
      display: inline-block; background: var(--accent); color: #fff; text-decoration: none;
      font-weight: 600; padding: 11px 22px; border-radius: 8px;
    }
    .signin-btn:hover { background: var(--accent-press); }
/* Completed-work view controls (6.97.0). Sits above the tree toolbar and borrows the Tickets bar's shape, so
   the two "pick a subset then filter" screens look like siblings. Explicit background/color on the select for
   the usual reason — a bare one renders white and ignores the dark theme. */

/* "21 of 150 in the catalog — 129 annual controls fall in other quarters" (6.98.0). Quiet but reachable: the
   number it explains is the one people came to the screen to read. */
.s2-scope { color: var(--muted); cursor: help; border-bottom: 1px dotted var(--line); }

/* SOC 2 evidence query, in the catalog editor (6.101.0). A `.ui-callout` since 7.441.0, with the accent rule when
   the control pulls its own evidence: that behaves differently from one a technician collects by hand, and it
   should be visible at a glance while scrolling a 150-control catalog. A bare checkbox renders white and ignores
   the theme, so the dialog's checkboxes are tinted here. */
#soc2cat-body input[type="checkbox"] { accent-color: var(--accent); }
html[data-theme="dark"] #soc2cat-body input[type="checkbox"] { color-scheme: dark; }
.s2-ev-q-ran { font-size: 11px; color: var(--muted); margin-top: 8px; overflow-wrap: anywhere; }

/* One row per evidence file (6.102.0). A control routinely needs several exports, so the rows are visually
   separated — three unlabeled query boxes in a stack would be unreadable at a 150-control scale. */
/* Each row is its own box (7.441.0 kept it): a file name, a report and a query, and the preview of what it returns. */
.s2-ev-row { padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.s2-ev-row-top { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
/* `min-width: 0`, and a select no wider than its field: a select sizes to its longest option, and a long report
   name pushed the row 8 to 19px past the dialog on a phone. */
.s2-ev-row-top > .ui-field { flex: 1 1 200px; min-width: 0; }
.s2-ev-row-top select { max-width: 100%; }
.s2-ev-del { flex: 0 0 auto; }

/* Hiding a Kanban column (6.106.0). Standup asked for it: a long Completed column pushes the rest off screen.
   The × only shows on hover/focus so a five-column board isn't five little buttons at rest, but it stays
   keyboard-reachable — :focus-visible is why the rule isn't hover alone. */
/* 6.106.1: the × was `position: absolute; right: 0`, which put it directly on top of the count badge — the
   header is already a flex row with `justify-content: space-between`, so that badge is pinned to the right
   edge. It is a normal flex item now, sitting just after the count, and `align-items: center` lines the three
   up. Reserving its width even at `opacity: 0` is deliberate: the header must not reflow on hover. */
.pl-colhead { align-items: center; gap: 6px; justify-content: flex-start; }
.pl-count { margin-left: auto; }        /* keeps the badge hard right, as space-between used to */
/* 11.3x16 before 7.112.0 — the smallest target in the app, and it hides a column, which is not a
   trivial thing to hit by accident. The floor is reached with the box: `inline-flex` plus a
   `min-width`/`min-height`, leaving the glyph at 16px. The left padding goes because the box now
   supplies the spacing. */
.pl-colhide { flex: 0 0 auto; border: 0; background: transparent; cursor: pointer; color: var(--muted);
  font-size: 16px; line-height: 1; padding: 0; opacity: 0; transition: opacity .12s;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px; }
.pl-col:hover .pl-colhide, .pl-colhide:focus-visible { opacity: 1; }
/* The hover also sets the wash (7.457.0): without the default's fill, a text-color change alone is too faint a cue (1.7-2.6:1 between rest and hover). */
.pl-colhide:hover { color: var(--text); background: var(--ui-wash); }
/* A hidden column comes back from a `.ui-chip-btn` in the bar above the board (7.456.0). Its dashed edge is kept: it is
   the one thing that says the column is hidden rather than filtered out. */
[data-colshow] { border-style: dashed; }


/* --- Gantt View Planning (6.158.0) -------------------------------------------------------------- */
/* Geometry notes, because this screen is the only one that positions anything proportionally:
   - The label gutter is a fixed column and the track is the remainder, so every row's track starts at
     the same x. That is what makes bars in different rows comparable at all.
   - Bars are placed with left/width PERCENTAGES of the track, so the chart reflows with the window
     without a resize listener or a measure pass.
   - The grid lines, sprint bands and today marker live in one absolutely-positioned overlay behind the
     rows rather than repeating per row: N rows x M weeks of divs is the version that gets slow.
   - `.gt-body` is the positioning context for that overlay, so it must stay `position: relative`. */
.gt-wrap { display: flex; flex-direction: column; gap: 14px; --gt-gutter: 340px; }
.gt-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; }
.gt-headline h3 { margin: 0 0 2px; font-size: 17px; }
.gt-note { margin: 0; color: var(--muted); font-size: 12px; max-width: 62ch; line-height: 1.45; }
.gt-tools { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.gt-stat { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

.gt-chart { border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
  /* A scroll PANE in both axes, not a tall block. Two reasons, and the second is the load-bearing one:
     - the chart can be wider than the column on a narrow window, and it must scroll in its own box so
       the page body never scrolls sideways;
     - `position: sticky` resolves against the nearest SCROLLING ancestor. While the page was the
       scroller and this box only declared `overflow-x: auto`, a sticky axis stuck to a scrollport that
       never moved vertically — i.e. it was inert, which is why it was dropped rather than shipped in
       6.158.0. Giving the pane a height is what makes the sticky header actually work. */
  overflow: auto; max-height: min(74vh, 900px); overscroll-behavior: contain; }

.gt-axis, .gt-row { display: grid; grid-template-columns: var(--gt-gutter, 340px) 1fr; }
.gt-axis { border-bottom: 1px solid var(--line); background: var(--bg);
  position: sticky; top: 0; z-index: 4; }
.gt-axis-lbl { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* The axis track carries its own height and positioning context and deliberately does NOT also carry
   `.gt-track`. Both are single-class selectors, so they had equal specificity and the later one in this
   file won: `.gt-track`'s `height: 30px` beat this 40px, the track measured 30px, and the week ticks
   (top 0, 18px tall) and the sprint bands (bottom 2, 18px tall) overlapped by 8px — the date labels
   colliding with the sprint chips. Same trap as the `--gt-gutter` media query below. */
.gt-axis-track { position: relative; height: 40px; }
.gt-wk { position: absolute; top: 0; height: 18px; border-left: 1px solid var(--line);
  font-size: 10px; color: var(--muted); padding-left: 4px; overflow: hidden; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.gt-band { position: absolute; bottom: 2px; height: 18px; border-radius: 4px; overflow: hidden;
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
  /* --text, not --muted (7.88.0): muted has only +0.33 of headroom on --bg, so ANY tint under it
     fails. The band stays quiet through its fill and 10px size, not by dimming a label below
     readable. `.gt-band-cur` keeps its distinction on weight, border and a doubled tint. */
  font-size: 10px; line-height: 16px; padding: 0 5px; color: var(--text); white-space: nowrap; }
.gt-band-cur { background: color-mix(in srgb, var(--brand) 24%, transparent);
  border-color: var(--brand); color: var(--text); font-weight: 600; }

.gt-body { position: relative; }
.gt-underlay, .gt-topline { position: absolute; top: 0; bottom: 0;
  left: var(--gt-gutter); right: 0; height: auto; pointer-events: none; }
.gt-underlay { z-index: 0; }
.gt-topline { z-index: 2; }
.gt-gl { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line); opacity: .5; }
.gt-today { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--warn);
  opacity: .85; }
.gt-rows { position: relative; z-index: 1; }

.gt-group + .gt-group { border-top: 1px solid var(--line); }
.gt-row { min-height: 30px; align-items: center; }
.gt-label { padding: 4px 10px; display: flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 12px; position: sticky; left: 0; z-index: 3; }
/* A task label is a COLUMN of two blocks — `#id + title`, then `assignee + priority` — which is exactly
   the shape the milestone and project headings already use, so it reuses their `.gt-ltop` / `.gt-gsub`
   classes rather than inventing a parallel one.
   
   This replaced a single wrapping flex line, and the reason is worth recording. Flex decides where to
   wrap from items' BASE sizes, before any of them grow: with the title at `flex-basis: 0` the id, title,
   assignee and badge all fitted on line one, the title then grew into what was left, and it wrapped
   inside a ~170px box while the space under the assignee sat empty — a full title in five narrow lines
   where three wide ones would do. Stacking the blocks gives the title the whole gutter and needs no
   flex-wrap tuning at all. */
.gt-rows .gt-row:not(.gt-grouprow):not(.gt-prow) .gt-label {
  flex-direction: column; align-items: stretch; justify-content: start; gap: 1px;
  padding-top: 6px; padding-bottom: 6px; }
/* `start`, not the `center` the headings use: their first line is a single line, a task's may be four,
   and `#101` belongs beside the title's FIRST line rather than halfway down it. */
.gt-rows .gt-row:not(.gt-grouprow):not(.gt-prow) .gt-ltop { align-items: start; }
/* And the row aligns its two cells to the TOP, not the middle. `.gt-track` is a fixed 30px, so once a
   label wraps to three lines the default `center` left the bar floating halfway down a 110px row, level
   with the middle of the title and beside nothing in particular. Top-aligned, the bar sits against the
   title's first line — which is the line that names the task. Single-line rows are unaffected: label and
   track are both 30px there, so center and start agree. */
.gt-rows .gt-row:not(.gt-grouprow):not(.gt-prow) { align-items: start; }
/* The gutter stays put while the track scrolls under it, so these backgrounds must be OPAQUE — a
   translucent tint would let bars show through the labels. Hence `color-mix(... , var(--panel))`
   rather than `..., transparent)`, and hence the descendant selectors: `.gt-axis-lbl` and `.gt-label`
   are both single-class, so a plain `.gt-label { background }` would have beaten the axis one purely
   by being later in the file. */
.gt-rows .gt-label { background: var(--panel); }
.gt-rows .gt-grouprow .gt-label { background: color-mix(in srgb, var(--text) 4%, var(--panel)); }
.gt-rows .gt-prow .gt-label { background: color-mix(in srgb, var(--text) 9%, var(--panel)); }
.gt-axis .gt-label { background: var(--bg); z-index: 2; }
.gt-track { position: relative; height: 30px; }
/* A floor on the time column, so a genuinely cramped window scrolls the chart instead of shrinking the
   week columns past the point where a date fits in one. `17 Aug` at 10px needs about 40px including the
   tick padding, so ~400px is the point below which the axis stops being readable at all.
   This only ever binds on a small screen — measured at 1600, 1200 and 820px it does not bind, and the
   pane does not overflow. That is the fix for how 7.4.2 shipped it: at 620px and declared OUTSIDE the
   narrow media query its comment claimed it lived in, so it applied at every width and pushed 28-178px
   past the pane, clipping the last week column and the bars in it. It was also credited there with a
   title-readability gain that measurement attributes entirely to the wider gutter — with and without the
   floor, the median title reads 94% wide and 78% narrow. */
.gt-axis-track, .gt-row > .gt-track { min-width: 400px; }
.gt-id { color: var(--muted); font-variant-numeric: tabular-nums; flex: none; }

/* 7.9.1: the first column WRAPS, so the whole title and the whole assignee are readable.
   
   Everything below replaces a careful truncation scheme, and the reasoning is worth keeping because it
   is what motivated the change. The title and the assignee competed for one line of a 340px gutter, and
   the tuning was about who lost least: `.gt-who` at `flex: 0 3 auto` yielded three times as readily per
   pixel it occupied, which measured a median 89% of the title and 67% of the name at 1600px. That is a
   good answer to the wrong question — 89% of a title is still a title you cannot read, and the numbers
   only ever improved the *ratio* of two truncations.
   
   Wrapping removes the competition instead of tuning it. Nothing in this column is clipped now, so the
   shrink factors, the `max-width: 45%` and the three `text-overflow: ellipsis` rules are all gone: each
   was there only to choose what to hide. The row grows to fit its content, which is the honest layout
   for a column whose job is to say which task this is and whose it is.
   
   `overflow-wrap: anywhere` on the title because a work-item title can contain a long unbroken token — a
   path, a hostname, a ticket reference — and a single such word would otherwise push the gutter wide and
   reintroduce the clipping this removes. */
.gt-title { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* The assignee sits on the second block with the priority badge, which keeps `margin-left: auto` and so
   lands at the right-hand end of that line. Both are fully visible; neither competes with the title. */
.gt-who { color: var(--muted); font-size: 11px; flex: 0 1 auto; min-width: 0;
  overflow-wrap: anywhere; }
.gt-count { color: var(--muted); font-size: 11px; flex: none; }
.gt-none { font-style: italic; }
/* The sprint range in the header says the chart is a window rather than the whole plan, so it reads as a
   qualifier and not as a warning: an outlined `.ui-pill` with tabular figures since 7.449.0. */
/* Three tiers: project, milestone, task. The project row is the darkest band, the milestone a lighter
   one, and the indents step in so the hierarchy is readable without leader lines.
   The indents cost gutter, and the cost was measured rather than assumed: 26/40px gave a median title of
   88% and a median assignee of 54% ("Kenn…", which is not a name). 22/30 recovers the assignee to 67% and
   the title to 89%. Against 7.4.2, which had no indents at all, the three tiers cost about 5 points of
   title and 7 of assignee — paid for by milestone titles going from 57% clipped to none, because the
   project name moved off that row and onto its own. */
.gt-prow { background: color-mix(in srgb, var(--text) 9%, transparent); min-height: 42px; }
.gt-pgroup + .gt-pgroup { border-top: 1px solid var(--line); }
.gt-pkids .gt-grouprow .gt-label { padding-left: 22px; }
.gt-pkids .gt-row:not(.gt-grouprow) .gt-label { padding-left: 30px; }
/* 7.108.0 gave this a role and a tab stop so the keyboard could reach it, which also brought it
   inside the 24px floor — it measured 18px tall. Raising the box rather than the type keeps the
   heading reading as a heading. Only the opener form is a target: without `data-open` it is text. */
.gt-pt { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; }
.gt-pt[data-open], .gt-ms[data-open] { display: inline-flex; align-items: center; min-height: 24px; }
.gt-pt[data-open] { cursor: pointer; }
.gt-pt[data-open]:hover { text-decoration: underline; }
.gt-psum { border-color: color-mix(in srgb, var(--text) 65%, transparent); }
/* Collapsed: the children are gone, not faded. A half-hidden row still costs a row of height, which is
   the only thing collapsing is for. */
.gt-pgroup[data-collapsed] > .gt-pkids { display: none; }
.gt-group[data-collapsed] > .gt-row[data-id] { display: none; }
/* The first line of a group label: twisty plus title. `.gt-label` is a column on these rows, so the two
   have to share a row of their own. */
.gt-ltop { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* 24px, which is WCAG 2.5.8's floor, reached with the BOX and not with the glyph — the same fix
   6.130.0 used on the detections checkbox. The caret is drawn by `::before` at the center, so the
   button can grow without the triangle changing size or moving: it stays a 16px-looking control
   with a 24px target. Measured at 16x16 before this. */
.gt-tw { flex: none; min-width: 24px; min-height: 24px; padding: 0; border: 0; background: none;
  cursor: pointer; color: var(--muted); border-radius: 4px; position: relative; }
.gt-tw:hover { color: var(--text); background: color-mix(in srgb, var(--text) 10%, transparent); }
.gt-tw:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* A CSS triangle, so there is no glyph to go missing in a font and no image to load. */
.gt-tw::before { content: ""; position: absolute; top: 50%; left: 50%; width: 0; height: 0;
  border-left: 5px solid currentColor; border-top: 4px solid transparent;
  border-bottom: 4px solid transparent; transform: translate(-50%, -50%) rotate(90deg);
  transition: transform .12s ease; }
.gt-tw[aria-expanded="false"]::before { transform: translate(-50%, -50%) rotate(0deg); }
@media (prefers-reduced-motion: reduce) { .gt-tw::before { transition: none; } }

.gt-grouprow { background: color-mix(in srgb, var(--text) 4%, transparent); min-height: 42px; }
/* Two lines. On one line the milestone title and the project name both shrank, so both ellipsised to
   about 57% of themselves — two unreadable strings where the gutter had room for one readable one. */
.gt-grouprow .gt-label, .gt-prow .gt-label { flex-direction: column; align-items: stretch;
  justify-content: center; gap: 1px; }
.gt-gsub { display: flex; align-items: center; gap: 6px; min-width: 0; }
.gt-ms { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; }
.gt-ms[data-open] { cursor: pointer; }
.gt-ms[data-open]:hover { text-decoration: underline; }

.gt-bar, .gt-sum { position: absolute; border-radius: 5px; overflow: hidden; white-space: nowrap;
  font-size: 10px; }
.gt-bar { top: 6px; height: 18px; line-height: 18px; padding: 0 6px;
  background: var(--brand-bright); border: 1px solid var(--brand-bright);
  color: var(--on-brand-bright); }
/* `--on-accent`, matching `.gt-bar` directly above, which resolves its ink through
   `--on-brand-bright`. One of the 25 inline `#fff` that `theme.css` says are left alone; this one is
   converted because it is the odd sibling in a two-rule pair, not as a fix — the token is `#ffffff`
   today, so nothing changes until a brand overrides it. */
.gt-bar-cur { background: var(--accent); border-color: var(--accent); color: var(--on-accent);
  font-weight: 600; }
/* A rollup bar, not a highlight. `background: var(--text)` made this the brightest object on the
   screen — a near-white slab in dark mode — while being the least urgent thing on its row, and it read
   as a divider rule rather than as a bar. */
.gt-sum { top: 8px; height: 14px; line-height: 12px; padding: 0 6px; border-radius: 3px;
  background: color-mix(in srgb, var(--text) 16%, var(--panel));
  border: 1px solid color-mix(in srgb, var(--text) 42%, transparent);
  color: var(--text); font-weight: 600; }
.gt-bar-txt { overflow: hidden; text-overflow: ellipsis; display: block; }

.gt-row[data-id] { cursor: pointer; }
.gt-row[data-id]:hover { background: color-mix(in srgb, var(--brand) 8%, transparent); }
.gt-row[data-id]:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

@media (max-width: 900px) {
  /* The gutter shrinks before the track becomes unreadable. Declared AFTER the default below, because at
     equal specificity the later rule wins — with the default last, this never applied at any width. */
  .gt-wrap { --gt-gutter: 240px; }
  /* Below this width the gutter cannot hold an id, a title and a name: with all three the title showed a
     median 60% of itself. The name is the one to drop — the row is clickable and its tooltip and
     aria-label both still carry the assignee. Scoped to `.gt-rows` so the unplaced list below the chart,
     where `.gt-who` holds the REASON a task could not be placed, keeps it. */
  .gt-rows .gt-who { display: none; }
}

/* Priority badge placements on the two screen surfaces (7.5.0). Both are flex items; see cards.css. */
/* Kanban card: the title line becomes a row so the badge takes the card's top-right corner and the
   title ellipsizes beside it rather than running underneath. */
/* It keeps the board's own title type, 13px semibold, rather than `.ui-card__title`'s 14px (7.456.0). */
.pl-title { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; font-weight: 600; }
.pl-title-text { flex: 1 1 auto; min-width: 0; }
/* Gantt row: the last item of the label column, which already lets the title take the free space and the
   assignee give way. `flex: none` keeps the badge a fixed size so the badges line up down the chart. */
.gt-label .prio-badge { margin-left: auto; }

/* ---- The mileage report screen (7.7.0; migrated to the component layer in 7.78.0) --------------
   Ten of this screen's thirteen rules are gone, into `.ui-toolbar`, `.ui-field`, `.ui-panel`,
   `.ui-table`, `.ui-num`, `.ui-btn` and `.ui-empty`. What is left is what a component should NOT own:
   this screen's own page layout, and one column width. That is the division the layer is for — a
   component knows what a table looks like, and only this screen knows that its Card column should stop
   at 320px. The page's column is `.ui-stack` since 7.393.0, which is what `.ml-wrap` drew.

   The dark-mode rule that used to sit here went too, and for a different reason: it was measured
   redundant, not replaced. See `test_the_mileage_native_controls_are_themed_for_dark_mode`. */
.ml-head { font-size: 14px; }
/* The two rollups sit side by side and drop to one column when there is no room. The panels inside are
   `.ui-panel`; this is only the arrangement, which is the screen's business. */
/* `flex-start`, not the default `stretch`: the two rollups rarely have the same number of rows, and a
   one-row "By person" stretched to the height of a three-row "By client" is a panel mostly made of
   empty surface. Seen in the browser; the old `.ml-sum` cards had the same behavior and nobody had
   looked at them side by side with uneven data. */
.ml-sums { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; }
.ml-sums > .ui-panel { flex: 1 1 240px; min-width: 0; }
/* A card's title is the one cell here that can be long enough to push the table sideways. */
.ml-card { max-width: 320px; }

/* --- Gantt: drag a bar onto another sprint to replan it (7.9.0) --- */
/* The grab cursor is the whole affordance, so it appears only when the gesture is actually
   available — a grab cursor on a bar that cannot move is a lie about what the chart does. */
.gt-bar-drag { cursor: grab; }
/* z-index 2 reuses the today-marker rung of the Gantt's own local ladder: above the other bars,
   still BELOW the sticky label gutter at 3, so a dragged bar scrolls under the gutter exactly as
   a stationary one does. A sixth rung would have meant a new number for no new relationship. */
.gt-bar-dragging { cursor: grabbing; opacity: .85; box-shadow: 0 2px 10px rgba(0,0,0,.35); z-index: 2; }
/* The band under the cursor. Outlined rather than filled: the bands carry their own color for the
   current sprint, and repainting them would lose that while dragging. */
.gt-band-drop { outline: 2px solid var(--accent); outline-offset: -2px; }
.gt-replan { padding: 8px 0 10px; }
/* The today line is drawn over the rows (`.gt-topline` at 2, `.gt-rows` at 1), so it ran through the open form's
   fields (7.447.0). The form is inside `.gt-rows`, whose z-index makes it a stacking context, so no z-index on the
   form can lift it past the line: the rows are lifted instead, and only while a form is open. Still under the
   sticky axis at 4. The bars sit over the line until the form closes. */
.gt-rows:has(.gt-replan) { z-index: 3; }
.gt-replan-in { display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
                padding: 10px 12px; background: var(--panel-2); border: 1px solid var(--line);
                border-radius: 8px; }
.gt-replan-in input[type="text"] { min-width: 12rem; }
.gt-replan-what { font-family: ui-monospace, monospace; font-size: .86rem; color: var(--muted); }

/* --- One project's whole plan (7.9.2) --- */
/* The base floor stays a plain 400px: it is the "an axis narrower than this stops being readable" rule,
   and a test asserts gutter + floor fits the pane at full screen. The scaled width is a SEPARATE rule so
   that guard keeps its literal, and so it applies only where overflowing is the point. */
/* `max(100%, ...)`, not the calc alone: a span just past the threshold can still be narrower than the
   pane, and a track narrower than its own scrollport leaves a gap to the right of the chart. The floor of
   100% means the axis always fills the pane and only grows past it, which is what turns a long plan into
   a scroll rather than a squeeze. */
.gt-wrap[data-wide] .gt-axis-track,
.gt-wrap[data-wide] .gt-row > .gt-track { min-width: max(100%, calc(var(--gt-weeks, 10) * 62px)); }
/* The scope chip: what the chart is filtered to, and the way out of it. Sits in the toolbar with the
   board picker, because it is the same kind of thing — a filter on what you are looking at. */
.gt-scoped { display: inline-flex; align-items: center; gap: 6px; font-size: 12px;
  padding: 3px 4px 3px 9px; border-radius: 999px; background: color-mix(in srgb, var(--brand) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--brand) 45%, transparent); max-width: 22rem; }
.gt-scoped-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Measured 16.8x15 before 7.102.0, and it is the only way out of a project-scoped chart, so it is
   the worst control on the screen to have to aim at. A flex box at the floor rather than more
   padding, which would have stretched the chip. */
.gt-scoped-x { border: 0; background: none; color: inherit; cursor: pointer; font-size: 15px;
  line-height: 1; padding: 0; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px; }
.gt-scoped-x:hover { background: color-mix(in srgb, var(--text) 14%, transparent); }
.gt-scoped-x:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* The Plan button, on a Gantt project heading and on a project row in the tree screens, is one `.ui-chip-btn`
   since 7.449.0. It was two private copies, `.gt-plan-btn` and `.tplan`, and the second missed the 24px floor
   the first reached in 7.102.0, which is what two copies of one control do. On the heading it is pushed to the
   right end so it never interrupts reading the project name; in the tree it sits in `.tmeta` with the pills. */
.gt-ltop > [data-plan] { margin-left: auto; flex: none; }

/* --- SoW contracts (7.27.0; migrated to the component layer in 7.80.0) ---------------------------
   Twenty of this screen's rules are gone, into `.ui-toolbar`, `.ui-field`, `.ui-field-grid`,
   `.ui-panel`, `.ui-table`, `.ui-pill`, `.ui-btn` and `.ui-empty`. What is left below is what a
   component should not own: this screen's own row layouts, its column widths, and the code chip that
   makes a SoW number look like the tag it is. 7.404.0 moved the page column, the headings, the hints, the
   messages and the totals onto `.ui-stack`, `.ui-overline`, `.ui-field__hint` and `.ui-status`. */
/* The number is shown as a code chip because it is a *tag* — the same string has to be typed onto a
   card, and styling it as prose invites retyping it slightly differently. */
.sow-tag { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px;
  white-space: nowrap; }
.sow-rates { max-width: 340px; color: var(--muted); }
.sow-acts, .sow-file { white-space: nowrap; }

/* The control painting that used to live here is gone, and the comment above it was the reason to
   look: it said "**this app has no global input styling — each screen states its own**". That stopped
   being true in 7.28.2, which put a default for every text input, select and textarea in `base.css`
   precisely so a screen whose author forgot would stop shipping white boxes. The rules here were a
   second, identical copy — and a stale comment explaining why something is impossible is how it stays
   impossible.

   Removing them was verified by measuring, not by reading the two rulesets and judging them
   equivalent: the computed styles of all 21 controls on this form were captured with the old
   declarations re-injected and again without them. 32 differences came back, in three properties, and
   only one of the three is real:

     * **font-weight 500 -> 400, on every control.** Real, visible, and deliberately accepted. `base.css`
       states no font-weight on purpose (its own comment records that adding one moved ~300 already-
       styled controls across the app), so 500 was this screen's idiosyncrasy and 400 is what every
       other control gets — including the mileage screen migrated in 7.78.0. Keeping 500 here would
       mean two migrated screens whose inputs do not match, which is the exact inconsistency the layer
       exists to remove.
     * **width 100% -> auto**, on three controls. Not real: all three were `hidden` at the moment of
       measurement (the new-client box, the once-only date, the default rate under a fixed-fee
       contract), and `auto` is simply what a `display:none` element computes. The visible controls
       showed no width difference at all.
     * **min-width 0 -> auto**, on the grid fields. Not real either: these are children of a COLUMN
       flex, where min-width constrains the cross axis and `align-items: stretch` already sizes them to
       the container. `.ui-field` carries `min-width: 0` itself, which is where it actually matters —
       on the grid child.

   The three ROW editors need it, because there it does matter: they are horizontal flex, so min-width
   governs the main axis, and without it a long placeholder stops the input shrinking and the row
   overflows. `base.css` rightly does not set this — it is a property of being a flex child, not of being
   an input — and since 7.377.0 the editors are `.ui-rows`, whose row sets it on every control. */
/* The duplicate hint under a typed client name. `--warn`, not `--err`: it is a question, not a refusal. A
   `.ui-field__hint` at the hint's size, because it sits with the field's other hints; the component has no
   tone, so the color stays here. */
.sow-client-warn { color: var(--warn); }
.sow-client-warn[hidden] { display: none; }
#sow-client-new[hidden] { display: none; }
/* Billed items on a fixed-price contract. Its rows, the rate card's and the milestones' are one component,
   `.ui-rows--tight` (7.377.0): the same kind of list, and looking alike is what makes that obvious. */
.sow-line-load { margin-bottom: 8px; }
.sow-line-load[hidden] { display: none; }

/* Both overlay panes are `.ui-panel` now. `overflow: hidden` is the one thing they need on top of it,
   and only because of what they contain: an iframe and a list of bordered rows both run square corners
   out past the panel's radius. A component cannot know that, so it stays here. */
.sow-pane { overflow: hidden; }
/* The preview is an iframe on our own endpoint, never a SharePoint link — the server streams the bytes
   behind the same grant as the rates. Given a real height here because a PDF in a zero-height frame is
   an empty box that reads as a broken document rather than as a layout bug. */
.sow-preview-meta { color: var(--muted); font-size: 12px; margin-right: auto; }
.sow-preview-frame { width: 100%; height: min(70vh, 800px); border: 0; background: var(--panel-2); }


/* --- SoW billing schedules and milestones (7.28.0) --- */
.sow-bills { max-width: 220px; }
/* "Due" is the only state on this screen that asks somebody to go and do something, so it is the only
   one that gets color. Everything else is a date to watch. */
.sow-due { color: var(--warn); font-weight: 600; }

.sow-ms-pane-rows { display: flex; flex-direction: column; }
.sow-ms-pane-row { display: flex; gap: 12px; align-items: center; padding: 7px 12px;
  border-bottom: 1px solid var(--line); font-size: 13px; }
.sow-ms-pane-row:last-child { border-bottom: 0; }
.sow-ms-pane-name { flex: 1 1 auto; font-weight: 600; }
.sow-ms-pane-due, .sow-ms-pane-amt { color: var(--muted); font-variant-numeric: tabular-nums; }

    /* --- Audit evidence (7.35.0) ------------------------------------------------------
       The collected files under a control, and the pane one opens into. The pane streams from our own
       endpoint, never a SharePoint link, so it stays inside the app's grant and its record of who looked. */
    .s2-files { margin-top: 6px; }
    /* 18px before 7.112.0, and there is one per control row, so it is the most repeated
       under-floor target on the audits screen. */
    .s2-files > summary { cursor: pointer; font-size: 12px; color: var(--muted);
      display: inline-flex; align-items: center; min-height: 24px; }
    .s2-files > summary:hover { color: var(--text); }
    .s2-evlist { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
    /* A collected file is a `.ui-files__item--row` since 7.432.0. The list stays a column, one row per line:
       `.ui-files` lays rows side by side. */
    /* Why Approve isn't offered on a file you collected (7.58.0). Its own line, muted — this is the
       normal state of affairs on a small team, not a failure, so it should not read as one. */
    .s2-ev-blocked { flex-basis: 100%; color: var(--muted); font-size: 11px; overflow-wrap: anywhere; }
    /* Actions sit in their own box that never shrinks — the 7.28.4 rule: the name loses characters
       before the only button does. */
    .s2-ev-acts { margin-left: auto; flex: 0 0 auto; display: inline-flex; flex-wrap: wrap; gap: var(--ui-space-3); }
    /* The per-row actions column on the plan table. Narrow and never the thing that wraps: the kebab is
       a fixed 24px target and the columns beside it carry the text. */
    .s2-rowacts { width: 1%; white-space: nowrap; text-align: right; vertical-align: top; }
    /* The menu itself is the host's `.card-menu` — this only positions it in the cell. Reusing those
       classes is what brings the outside-click close, the Escape handling and the [hidden] reassert. */
    .s2-rowmenu { display: inline-block; }

    /* Evidence already in the control's folder that did not come from this card. Separated by a rule
       rather than a heading weight alone, because the two lists answer different questions: what is
       attached here, and what a reviewer will find in the folder. */
    .s2-cardev-also { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line); }
    /* Muted against the card's own rows: these are elsewhere, and the card is not where they are
       managed — the audits screen is. */
    .s2-cardev-folder { color: var(--muted); }
    /* An evidence file's state is a `.ui-pill--soft` in its tone since 7.422.0 (`evidenceStatePill`). The two
       broken-seal states keep a heavier tint of the error color and text-colored bold on top of it; written
       on `.ui-pill` so they outrank the component's soft tint. */
    /* Changed after approval is the finding an assessor is looking for, so it is the one state that
       carries weight as well as color — color alone is not a signal for every reader. */
    .ui-pill.s2-ev-superseded { background: color-mix(in srgb, var(--err) 32%, transparent); color: var(--text);
                        font-weight: 700; }

    /* The pane is a `.ui-panel` since 7.429.0. This keeps the space around it, which the panel does not set,
       and clips the document frame to the panel's rounded corners. */
    .s2-ev-pane { margin: 10px 0; overflow: hidden; }
    /* A document viewer's size, which no component sets. The rule above it separates the notice from the
       document, whose own page is usually white. */
    .s2-ev-frame { display: block; width: 100%; height: 60vh; min-height: 320px; border: 0;
                   border-top: 1px solid var(--line); background: var(--panel); }
    .s2-ev-add { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
    /* The picker is a <label> wrapping a hidden <input type=file>, so it needs the button's own
       affordances — a label is not focusable and gets no pointer by default. */
    .s2-ev-pick { cursor: pointer; }

    /* --- Custom roles (7.36.0) --------------------------------------------------------- */
    /* A role that can't draw the board (7.57.0). The warn color, not the error one: the grid is saveable
       and the instance may mean it — a role granted alongside another is a real thing. `cursor: help`
       because the title is where the actual sentence is. */
    .ac-warn { color: var(--warn); font-size: 12px; cursor: help; }

    /* --- A broken seal (7.38.0) -------------------------------------------------------
       The finding an assessor is looking for, so it reads as one: its own line, the error token, and a
       rule down the side. Weight and position carry it as well as color — color alone is not a signal
       for every reader. */
    .s2-ev-alarm { border-left: 3px solid var(--err); padding-left: 8px; }
    .s2-ev-broken { flex-basis: 100%; font-size: 11px; color: var(--err); font-weight: 600;
                    overflow-wrap: anywhere; }
    .ui-pill.s2-ev-removed { background: color-mix(in srgb, var(--err) 32%, transparent); color: var(--text);
                     font-weight: 700; }
    /* A file the folder no longer holds: the name is struck through, because the row is about something
       that is not there. */
    li.s2-ev-alarm:has(.s2-ev-removed) .ui-files__name { text-decoration: line-through; }
