:root {
  --error: #e74c3c; --critical: #c0392b; --warning: #f39c12;
  --info: #3498db; --debug: #95a5a6;
  --bg: #f5f6f8; --card: #fff; --ink: #2c3e50; --muted: #7f8c8d; --line: #e3e6ea;
  --link: #2f6fed; --ok-bg: #e8f5e9;
  /* Key Cyan — the one hue outside the severity scale, and it marks exactly one
     thing: the payload values that fed the fingerprint. Two surfaces, same mark:
     cyan on the dark payload block (10.7:1 on #1f2430), teal for the legend swatch
     on the light field (4.9:1 on --bg). See DESIGN.md § Colors. */
  --key-cyan: #67e8f9; --key-cyan-ink: #0e7490; --key-cyan-tint: rgba(103, 232, 249, .13);
  /* Text on Console Dark — the three-step ramp every quoted-wire surface reads from
     (payload, stack trace). All three clear AA on #1f2430; see DESIGN.md
     § Evidence surfaces, which is also why --wire-dim is the floor. */
  --wire-bright: #e6edf3; --wire-lifted: #cfd6e0; --wire-dim: #8b95a5;
  --state-new: #64748b; --state-work-item: #0e7490;
  --state-in-mr: #6d4fc4; --state-rolled-out: #157347;
  --state-resolved: #1a7a40; /* on --ok-bg: ≈4.9:1 */
  --filter-label-w: 5.5rem;  /* the shared label column of every .filter */
}
* { box-sizing: border-box; }
/* Clickable chrome shouldn't text-select on double/triple-click. Content
   (titles and payloads on detail pages, data tables) stays selectable. */
button, summary, nav a, .brand, a.dash, .chip, .badge, .pill, a.sortlink,
a.rowlink, .rowact, a.row, a.legend-item { user-select: none; -webkit-user-select: none; }
/* Stop iOS inflating monospace titles inside the 760px-wide (panning) errors
   table — Safari auto-sizes text in any block wider than the viewport.
   overflow-anchor: morph refreshes (sort, live updates) reorder rows in place;
   scroll anchoring would follow the moved row, so the page jumps on its own.
   The viewport must only move when the user scrolls. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; overflow-anchor: none; }
body { margin: 0; font: 14px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; background: var(--bg); color: var(--ink); }
a { color: inherit; text-decoration: none; }

/* Content links read as links: coloured + underlined. Structural links
   (whole-row cards, filter chips, primary actions, footer nav) opt out and
   keep their own styling — :where() zeroes the :not chain's specificity so
   any later component rule wins without a fight. */
.container a:where(:not(.row):not(.chip):not(.rowlink):not(.rowact):not(.sortlink):not(.switch):not(.primary-action):not(.headact):not(.kind-card)) { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
.container a:where(:not(.row):not(.chip):not(.rowlink):not(.rowact):not(.sortlink):not(.switch):not(.primary-action):not(.headact):not(.kind-card)):hover { text-decoration: underline; filter: brightness(0.85); }

/* sortable column headers keep header styling; active carries the ↓ */
a.sortlink { color: inherit; white-space: nowrap; }
a.sortlink:hover { text-decoration: underline; text-underline-offset: 2px; }
a.sortlink.active { font-weight: 700; }
table a { font-weight: 600; }

/* Nav entries (ApplicationHelper#nav_link + the project switcher's summary): the
   nav may never move under the pointer, and marking "you are here" with a heavier
   weight is what moved it — 600 is wider than 400, so the current entry grew and
   shoved everything after it sideways (3.6px in the topbar when the switcher took
   its chip, 6.5px in the footer nav). Every entry therefore reserves the width of
   its own bold rendering in both states: a zero-height, AT-hidden duplicate of the
   label at 600 (from data-label) sets the shrink-to-fit width, so the geometry is
   the same on every page. Weight, color and the underline seam still change — only
   the box stays put. Any new nav entry goes through nav_link and inherits this. */
.nav-label { white-space: nowrap; }
.nav-label::after {
  content: attr(data-label); display: block; font-weight: 600;
  height: 0; overflow: hidden; visibility: hidden;
}

.topbar { display: flex; align-items: center; gap: 24px; background: #1f2430; color: #fff; padding: 0 24px; height: 52px; }
.topbar .brand { font-weight: 700; font-size: 16px; white-space: nowrap; }
/* Desktop: the plain nav renders inline (contents = children join the topbar
   flex); the hamburger doesn't exist. Mobile flips both (see @media below). */
.topbar .desktop-nav { display: contents; }
.topbar .mobile-menu { display: none; }
.topbar nav { display: flex; gap: 18px; align-items: center; }
.topbar nav a { color: #b9c2cf; padding: 4px 0; }
.topbar nav a:hover { color: #fff; }
/* the current section: white plus a quiet underline seam (never a level color) */
.topbar nav a.current { color: #fff; box-shadow: inset 0 -2px 0 #5b6b7b; }
/* standalone Dashboard link (left of the project switcher; hamburger on mobile).
   Same current treatment as a nav entry — it IS one, it just sits outside <nav>. */
.topbar > a.dash { color: #b9c2cf; padding: 4px 0; }
.topbar > a.dash:hover { color: #fff; }
.topbar > a.dash.current { color: #fff; box-shadow: inset 0 -2px 0 #5b6b7b; }

/* project switcher — details/summary dropdown (native select popups misalign on macOS).
   Two states, because the bar carries two scopes. At rest it is a control among the
   nav links: console-text on the bar, hairline border, regular weight — a way INTO a
   project. `.current` (only on /p/<name>/… pages) promotes it to the raised
   console-surface chip in white 600, where it names the scope the whole page below
   belongs to. Without that split the remembered project was the brightest thing in
   the bar on the cross-project dashboard, and read as that page's header. */
.topbar .project-switcher { position: relative; }
.topbar .project-switcher summary {
  list-style: none; display: inline-block;
  background: transparent url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23b9c2cf' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 10px center;
  color: #b9c2cf; border: 1px solid #3a4250; border-radius: 6px;
  padding: 5px 30px 5px 12px; font-size: 13px; font-weight: 400; cursor: pointer;
}
/* the page IS in this project: the switcher is its header, not an entrance
   (the width it takes doesn't change with that state — see .nav-label) */
.topbar .project-switcher.current summary { background-color: #2c3340; color: #fff; font-weight: 600; }
.topbar .project-switcher summary::-webkit-details-marker { display: none; }
.topbar .project-switcher summary:hover { color: #fff; border-color: #6b7686; }
.topbar .project-switcher[open] summary { color: #fff; border-color: #6b7686; }
.topbar .project-switcher .menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; min-width: 100%;
  background: #2c3340; border: 1px solid #3a4250; border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .4); padding: 4px; display: flex; flex-direction: column;
}
.topbar .project-switcher .menu a {
  color: #b9c2cf; padding: 6px 12px; border-radius: 5px; font-size: 13px; white-space: nowrap;
}
.topbar .project-switcher .menu a:hover { background: #3a4250; color: #fff; }
.topbar .project-switcher .menu a.active { color: #fff; font-weight: 600; }

.container { max-width: 1240px; margin: 24px auto; padding: 0 24px; }
h1 { font-size: 20px; margin: 0 0 16px; }
h2 { font-size: 15px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; margin: 28px 0 10px; }

.rows { background: var(--card); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.row { display: flex; align-items: center; gap: 14px; padding: 12px 16px; border-bottom: 1px solid var(--line); border-left: 4px solid transparent; }
.row:last-child { border-bottom: 0; }
.row:hover { background: #fafbfc; }
.row.error, .row.critical { border-left-color: var(--error); }
.row.warning { border-left-color: var(--warning); }
.row.info { border-left-color: var(--info); }
.row.debug { border-left-color: var(--debug); }

.row .title { flex: 1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .meta { color: var(--muted); font-size: 12px; white-space: nowrap; }

/* deploy time markers between feed rows — a quiet dashed seam, not a row */
.deploy-marker { display: flex; align-items: center; gap: 10px; padding: 5px 16px; font-size: 12px; color: var(--muted); background: #eef2f7; border-top: 1px dashed var(--line); border-bottom: 1px dashed var(--line); }
.deploy-marker .deploy-tag { font-size: 10px; font-weight: 700; letter-spacing: .06em; color: #5b6b7b; }
.deploy-marker span:nth-child(2) { flex: 1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deploy-marker .meta { white-space: nowrap; }
/* inside the errors table the row hairlines take over the seam */
tr.deploy-row td { padding: 0; }
tr.deploy-row .deploy-marker { border-top: 0; border-bottom: 0; }

/* deploys page: the history behind the markers — a headed table like the
   errors list. Deliberately NOT table.errors: that one's mobile treatment
   stacks rows into cards, which suits error triage but would mangle this
   ledger; deploys pan-scroll on phones instead (mobile block). */
table.deploys { table-layout: auto; width: 100%; border-collapse: separate; border-spacing: 0; }
/* the filtered count is the table's own caption — same text inset as the columns */
table.deploys caption { caption-side: top; text-align: left; padding: 0 16px 10px; color: var(--muted); font-size: 13px; }
table.deploys th { position: sticky; top: 0; background: var(--card); z-index: 10; }
table.deploys th.stamp, table.deploys th.ago { white-space: nowrap; }
table.deploys td.meta { color: var(--muted); font-size: 12px; white-space: nowrap; }
table.deploys .rev { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; min-width: 7ch; white-space: nowrap; }
/* the comment column absorbs free width and truncates at its cell edge */
table.deploys .title-cell { width: 100%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
table.deploys tbody tr:hover td { background: #fafbfc; }
table.deploys td.empty { padding: 40px; text-align: center; color: var(--muted); }
/* carried over from before a from-bounded range — already live, not shipped
   in-window. The seam speaks the deploy-marker dialect (dashed, hushed). */
table.deploys tr.carried td { background: #f8f9fb; }
table.deploys tr.carried-seam td { padding: 5px 16px; font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #5b6b7b; background: #eef2f7; border-top: 1px dashed var(--line); }
form.date-range input[type="date"] { padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; background: var(--card); color: var(--ink); }
form.date-range .range-inputs { display: flex; align-items: center; gap: 8px; } /* both dates wrap as one unit */
form.date-range .range-sep { color: var(--muted); }
.count { font-weight: 700; min-width: 48px; text-align: right; }

.badge { display: inline-block; font-size: 11px; font-weight: 700; text-transform: uppercase; padding: 2px 7px; border-radius: 3px; color: #fff; }
/* one level→color mapping for badges and active filter chips */
.badge.error, .badge.critical, .chip.level-error.active, .chip.level-critical.active { background: var(--error); border-color: var(--error); }
.badge.warning, .chip.level-warning.active { background: var(--warning); border-color: var(--warning); }
.badge.info, .chip.level-info.active { background: var(--info); border-color: var(--info); }
.badge.debug, .chip.level-debug.active { background: var(--debug); border-color: var(--debug); }

/* workflow-state badge (own State column / next to the level badge). The
   state palette is its own (slate → teal → violet → green, all ≥4.5:1 with
   white text) — the five severity colors keep their monopoly. State text
   stays lowercase: it's a description, not an alarm. */
.badge.state { text-transform: none; font-weight: 600; white-space: nowrap; }
.badge.state .icon { display: none; } /* mobile swaps label → icon (see @media) */
.badge.state-new         { background: var(--state-new); }
.badge.state-work-item   { background: var(--state-work-item); }
.badge.state-in-mr       { background: var(--state-in-mr); }
.badge.state-rolled-out  { background: var(--state-rolled-out); }
/* terminal state: quiet tint, not another saturated chip — the row is settled */
.badge.state-resolved    { background: var(--ok-bg); color: var(--state-resolved); }
table.errors td.state .badge { vertical-align: middle; }

.pill { display: inline-block; background: #eef1f4; color: var(--muted); border-radius: 10px; padding: 1px 9px; font-size: 12px; white-space: nowrap; }
/* resolved state: green check on the error page pill and the index rows */
.pill.ok { background: var(--ok-bg); color: #1e8e3e; }
.empty { padding: 40px; text-align: center; color: var(--muted); }

/* Infinite-scroll sentinel: a quiet "loading more" line at the foot of a feed
   (a table row on the errors grid, a plain block in the occurrences list). */
.load-more, td.load-more { padding: 16px; text-align: center; color: var(--muted); font-size: 12px; }

/* error page header: title column left (long titles wrap inside it),
   actions pinned top-right; mobile stacks (see @media block) */
/* one shared center line: h1 text, ⓘ, and action buttons all align on it —
   the h1 carries no own margin here (it would skew the centering) */
/* project scope line above the error header — which fleet this error belongs to */
p.meta .project-name { color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
a.project-name:hover { text-decoration: underline; text-underline-offset: 2px; }
p.meta { margin-bottom: 4px; }
.error-head { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; }
.error-head h1 { flex: 1; min-width: 0; margin-bottom: 0; overflow-wrap: anywhere; }
/* state pills (group · N errors / ✓ Resolved) trail the title: a deliberate
   12px gap off the heading text, optically centered against the big bold h1 */
.error-head h1 .pill { margin-left: 12px; vertical-align: middle; }
/* level + state badges lead the title (detail pages' h1): gap after each,
   optically centered against the big bold heading text */
h1 .badge { margin-right: 8px; vertical-align: middle; }
.head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.head-actions form.inline { margin-left: 0; }

/* summary line: two groups, one row — the error's facts (recency, volume) and the
   links out to the trackers. Spacing carries the distinction: 6px inside a group,
   16px between them, and each group wraps as a whole so refs never interleave with
   the facts. Only the ticket chips are bordered, which is all the separation the
   two kinds need — no rule, no second background. */
.summary { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; color: var(--ink); font-size: 13px; margin: 0 0 12px; }
.summary .facts, .summary .links { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.summary .dot { color: var(--muted); }
/* one link = one chip: kind, ref, remove. The chip recipe (card-white, hairline,
   pill radius) is the app's, and the parts inside it stop being three floating
   pieces the eye has to assemble. */
.summary .ticket { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 2px 0 10px;
                   background: var(--card); border: 1px solid var(--line); border-radius: 13px; }
.summary .ticket .kind { color: var(--muted); font-size: 12px; }
.summary form.unlink-form { margin: 0; display: flex; }
/* × unlink: the round ghost target the search field's clear button already is —
   fog at rest, error-red on hover/focus, never filled red (danger-ghost rule).
   Shared with the comment cards' × so both are the same object. */
button.unlink { display: grid; place-items: center; width: 22px; height: 22px; padding: 0;
                background: none; border: 0; border-radius: 50%; color: var(--muted);
                cursor: pointer; font-size: 15px; line-height: 1; }
button.unlink:hover, button.unlink:focus-visible { background: var(--bg); color: var(--error); }
button.unlink:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }
/* The person mark. One round object at row height, and the picture sits ON TOP of
   the initials rather than instead of them: both sources fail routinely and
   legitimately (Google rotates its avatar URLs, and we ask Gravatar with d=404 so
   an unknown address answers with an error rather than a stranger's silhouette),
   so avatar_controller.js removes the <img> and the letters underneath are already
   in place — no second request, no reflow, never a broken-image glyph in a table
   row. #4a5568 on #dfe4ea is 5.9:1. */
.avatar { position: relative; display: inline-grid; place-items: center; flex: none;
          width: 22px; height: 22px; border-radius: 50%; overflow: hidden;
          background: #dfe4ea; color: #4a5568; font-size: 10px; font-weight: 700;
          line-height: 1; vertical-align: middle; }
.avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Who owns the error, leading the summary line: the mark, the name, and the way to
   hand it on. The Assign/Reassign disclosure wears the same chip-ghost recipe as
   + Link (see below) — one vocabulary for "an action sitting in this row". */
.summary .owner { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.summary .assignee { display: inline-flex; align-items: center; gap: 6px; }
.summary .who-name { white-space: nowrap; }
details.assign { position: relative; }
/* the people list: one row per person, each the whole click target */
details.assign .menu ul.people { list-style: none; margin: 0; padding: 0; min-width: 200px; }
details.assign .menu form.person-form { margin: 0; }
details.assign .menu button.person {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px;
  background: none; border: 0; border-radius: 6px; color: var(--ink);
  font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
details.assign .menu button.person:hover { background: var(--bg); }
details.assign .menu button.person.current { font-weight: 600; }
details.assign .menu button.person .has-it { margin-left: auto; color: var(--state-work-item); }
details.assign .menu button.person:focus-visible { outline: 2px solid var(--link); outline-offset: -2px; }
/* giving the error back is the destructive end of the list — ghost, never filled */
details.assign .menu button.person.unassign { color: var(--muted); margin-top: 4px; border-top: 1px solid var(--line); border-radius: 0 0 6px 6px; }
details.assign .menu button.person.unassign:hover { color: var(--error); background: var(--bg); }

/* + Link disclosure: a chip-shaped ghost button, so "add a link" reads as an
   action sitting in the chip row rather than as a stray blue word at the end of a
   sentence of facts. */
details.add-link, details.edit-integrations { position: relative; }
details.add-link summary, details.edit-integrations summary, details.assign summary {
                           list-style: none; cursor: pointer; display: inline-flex; align-items: center;
                           height: 26px; padding: 0 11px; background: var(--card);
                           border: 1px solid var(--line); border-radius: 13px;
                           color: var(--muted); font-size: 13px; }
details.add-link summary:hover, details.add-link[open] summary,
details.edit-integrations summary:hover, details.edit-integrations[open] summary,
details.assign summary:hover, details.assign[open] summary { color: var(--ink); border-color: var(--muted); }
details.add-link summary:focus-visible, details.edit-integrations summary:focus-visible, details.assign summary:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
details.add-link summary::-webkit-details-marker, details.edit-integrations summary::-webkit-details-marker, details.assign summary::-webkit-details-marker { display: none; }
details.add-link .menu, details.edit-integrations .menu, details.assign .menu { position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; background: var(--card); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, .18); padding: 10px; }
form.addlink { display: flex; gap: 8px; align-items: center; }
form.addlink input[type="text"] { width: 320px; max-width: 70vw; padding: 7px 12px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }

/* discussion: short triage notes under the error, one quiet card per comment.
   The first comment also shows up top as a one-line note (.first-note). */
/* the first note, read as prose: author, note, and the way into the thread, in one
   flow behind a quiet left rule (the lightest member of the .comment card family —
   same quoted-human-text vocabulary, none of its weight). Full width like the cards
   it previews: a measure cap here reads as a block that stopped early, and it broke
   the line the same way the 720px cards did. */
.first-note { font-size: 13px; line-height: 1.55; color: var(--ink); margin: 0 0 16px;
              border-left: 2px solid var(--line); padding-left: 10px; }

/* rollout: one quiet line — how far the linked fix has travelled. The
   per-instance verdicts sit in the Affected instances table (.rollout-state);
   words carry the meaning, the tint only underlines it. */
.rollout { font-size: 13px; color: var(--ink); margin: 0 0 12px; }
.rollout .rollout-kind { color: var(--muted); margin-right: 4px; }
.rollout-state { font-size: 13px; white-space: nowrap; }
.rollout-verified, .rollout-deployed-since { color: var(--state-resolved); }
.rollout-awaiting, .rollout-unknown { color: var(--muted); }
.first-note .note-author { font-weight: 600; }
.first-note .note-body { margin-left: 6px; }
.first-note .note-more { white-space: nowrap; margin-left: 6px; }
/* Full width, like every other block on the page. A 720px measure is the kinder
   line length, but a card ending two thirds of the way across — under a full-width
   payload, table and vitals strip — reads as a layout that broke rather than as a
   deliberate column. */
.comment { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; }
.comment-head { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.comment-head .meta { color: var(--muted); }
.comment-head form.unlink-form { margin: 0 0 0 auto; }
/* A note is Markdown (MarkdownHelper), so the card has to host structure: the
   sanitizer's whole tag list gets a rule here, or a note eventually renders
   something with browser defaults in the middle of the app's type. */
.comment-body { font-size: 13px; }
.comment-body > :first-child { margin-top: 0; }
.comment-body p, .comment-body ul, .comment-body ol, .comment-body blockquote,
.comment-body pre, .comment-body table { margin: 6px 0 0; }
.comment-body ul, .comment-body ol { padding-left: 22px; }
.comment-body li + li { margin-top: 2px; }
.comment-body h3, .comment-body h4, .comment-body h5, .comment-body h6 {
  font-size: 13px; font-weight: 700; margin: 10px 0 0; }
.comment-body blockquote { padding-left: 10px; border-left: 2px solid var(--line); color: var(--muted); }
.comment-body hr { border: 0; border-top: 1px solid var(--line); margin: 10px 0 0; }
/* inline code takes the token recipe; a fenced block takes the payload surface
   (console-dark, the app's one quoted-code ground) at a card's scale */
.comment-body code { background: #eef1f4; padding: 1px 4px; border-radius: 4px; font-size: 12px; }
.comment-body pre { padding: 10px 12px; font-size: 12px; }
.comment-body pre code { background: none; padding: 0; font-size: inherit; color: inherit; }
form.comment-form { display: flex; gap: 8px; align-items: flex-start; margin-top: 8px; }
form.comment-form textarea { flex: 1; padding: 7px 12px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; font-family: inherit; resize: vertical; }
details.add-comment summary { list-style: none; cursor: pointer; color: var(--link); font-size: 13px; }
details.add-comment summary::-webkit-details-marker { display: none; }

/* diagnosis tabs: quiet underline vocabulary, content pre-rendered */
.tabs { margin-top: 28px; }
/* hairline as inset shadow, not border: the active tab's 2px underline can
   cover it without a -1px overhang (which reads as a scrollbar in the
   overflow-x container) */
.tab-bar { display: flex; gap: 4px; box-shadow: inset 0 -1px 0 var(--line); overflow-x: auto; }
.tab-bar .tab { background: none; border: 0; border-bottom: 2px solid transparent; padding: 8px 14px; font-size: 13px; color: var(--muted); cursor: pointer; white-space: nowrap; }
.tab-bar .tab:hover { color: var(--ink); }
.tab-bar .tab.active { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }
.tabs [role="tabpanel"] { padding-top: 14px; }
/* copy buttons float top-right inside the box they copy */
.copywrap { position: relative; }
.copywrap .overlay-actions { position: absolute; top: 10px; right: 10px; z-index: 5; display: flex; gap: 8px; }
p.viewall { margin: 14px 0 0; font-size: 13px; }

/* overview dashboard stat tiles */
/* one card divided by hairlines, like the error page's vitals strip — the same
   recipe for every row of numbers (three loose content-sized boxes read as the
   start of a widget wall, and their ragged widths gave the row no edge) */
.stat-row { display: flex; margin-bottom: 20px; background: var(--card); border: 1px solid var(--line); border-radius: 6px; }
.stat-card { flex: 1; padding: 12px 20px; border-left: 1px solid var(--line); }
.stat-card:first-child { border-left: 0; }
.stat-num { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
table.errors th.proj { width: 140px; }
/* the title-cell absorber squeezes fixed columns to min-content, which breaks
   hyphenated names ("gomus-shop") mid-word even on a wide screen — the name
   stays on one line and the title yields instead (it truncates anyway). The
   ellipsis cap keeps a silly-long name from stealing the row; the mobile
   stanza layout (media block at the bottom) re-allows wrapping. */
table.errors th.proj, table.errors td.proj { white-space: nowrap; }
table.errors td.proj { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; }

/* ===== Filter groups — the layout system for every filter bar =====
   One .filters grid per page holds ALL of that page's filters (instance,
   status, level, range, search, …), two per line; phones get one column
   (media block at the bottom). Each .filter inside is a label + control
   pair whose label sits in the shared fixed-width column
   (--filter-label-w), so labels align tabularly — side by side AND
   stacked. Controls (chips, dropdowns, inputs) wrap inside their cell.
   New filter = one more .filter child; never a filter outside .filters. */
.filters { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 32px; align-items: start; margin-bottom: 16px; }
.filters .filter { margin-bottom: 0; }
/* small phones only: the pair collapses to one stacked column */
@media (max-width: 520px) { .filters { grid-template-columns: minmax(0, 1fr); } }
/* Search field: the input and its clear button share one relative box, so the ✕
   sits inside the field's right edge (padding-right keeps text from running under
   it) and the box — not the input — is what flexes in the .filter row. */
form.search .search-field { position: relative; display: flex; flex: 1; max-width: 480px; }
form.search input[type="search"] { flex: 1; min-width: 0; padding: 8px 36px 8px 12px; border: 1px solid var(--line); border-radius: 6px; font-size: 14px; }
/* the engines that ship a cancel button each draw a different one and Firefox
   draws none — .clear-search replaces all of that with one control everywhere */
form.search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
/* same round-button recipe as .info-btn, sized for the field */
.clear-search {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 24px; aspect-ratio: 1; border-radius: 50%;
  display: grid; place-items: center; padding: 0;
  border: 1px solid var(--line); background: var(--card); color: var(--muted);
  font: 400 15px/1 -apple-system, "Segoe UI", Roboto, sans-serif;
  cursor: pointer;
}
.clear-search:hover { border-color: var(--muted); color: var(--ink); }
.clear-search:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
/* Nothing to clear, no button. :placeholder-shown reads the field's LIVE value,
   so this is not DOM state a morph refresh could reset (the field itself is
   data-turbo-permanent, so its value outlives the morph too). */
form.search:has(input:placeholder-shown) .clear-search { display: none; }

/* errors table — real columns, sticky header. NOTE: .table-scroll gets its
   overflow only in the mobile block — an overflow wrapper is a scroll
   container, and thead would stick to it instead of the viewport. */
/* Auto layout so the numeric/date columns hug their content; the Error column
   is the flexible absorber (width:100% + max-width:0 on .title-cell below), so
   free horizontal space widens the title instead of pooling as a gap beside it.
   min-width (mobile block) still forces the pan-scroll on narrow screens. */
table.errors { table-layout: auto; width: 100%; border-collapse: separate; border-spacing: 0; }
table.errors th { position: sticky; top: 0; background: var(--card); z-index: 10; }
table.errors th.sel { width: 28px; }
table.errors th.lvl { width: 96px; }
/* Wide enough for the 22px mark plus the cell padding, and no wider — the column
   exists so the title column does not have to carry a name. */
table.errors th.who { width: 44px; }
table.errors td.who { line-height: 1; }
/* numeric + date columns hug their header/value (nowrap keeps the sort label on
   one line); no fixed width so their unused room flows to the Error column */
table.errors th.num, table.errors th.ago { white-space: nowrap; }
table.errors th.num, table.errors td.num { text-align: right; }
table.errors td.num, table.errors td.ago { color: var(--muted); font-size: 12px; white-space: nowrap; }
table.errors td.num.strong { color: var(--ink); font-weight: 700; font-size: 13px; }
table.errors td:first-child { border-left: 4px solid transparent; }
/* the header row has no severity spine, so it needs the spine's width back as
   transparent border — otherwise every th sits 4px left of its column's cells,
   and the select-all box visibly misses the row boxes below it */
table.errors th:first-child { border-left: 4px solid transparent; }
table.errors tr.error td:first-child, table.errors tr.critical td:first-child { border-left-color: var(--error); }
table.errors tr.warning td:first-child { border-left-color: var(--warning); }
table.errors tr.info td:first-child { border-left-color: var(--info); }
table.errors tr.debug td:first-child { border-left-color: var(--debug); }
table.errors tbody tr:hover td { background: #fafbfc; }
/* Reappeared-after-resolve: resolved once, a new occurrence brought it back.
   Flagged inline in the open list — a dotted red box (the severity spine stays
   the left edge) with a tiny tag sitting on the top-right border. */
/* Extra top room holds the tag inside the row (a raised tag would read as
   belonging to the row above, and above the first row it hides behind the
   sticky header). */
table.errors tr.reappeared td { border-top: 1px dotted var(--error); border-bottom: 1px dotted var(--error); padding-top: 20px; }
table.errors tr.reappeared td:last-child { border-right: 1px dotted var(--error); position: relative; }
table.errors .reappeared-flag { position: absolute; top: 4px; right: 6px; font-size: 10px; line-height: 1.3; font-weight: 600; color: var(--error); white-space: nowrap; }
/* The dotted top would stack with the rule above it into a double line — drop
   that rule: the header's own when the reappeared row leads, else the preceding
   row's bottom (also collapses the seam between two adjacent reappeared rows). */
table.errors thead:has(+ tbody tr.reappeared:first-child) th,
table.errors tbody tr:has(+ tr.reappeared) td { border-bottom-color: transparent; }
/* the flexible column: width:100% claims all leftover space, max-width:0 lets
   it shrink and ellipsis-truncate at the true cell edge (no gap beside it) */
table.errors .title-cell { width: 100%; max-width: 0; overflow: hidden; }
table.errors .rowlink { display: flex; align-items: center; gap: 10px; color: inherit; font-weight: 400; }
table.errors .rowlink:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; border-radius: 3px; }
table.errors .rowlink .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
/* the group-count badge keeps its size; the title (min-width:0 above) is the one
   that shrinks and ellipsis-truncates, so "N errors" never wraps or gets clipped */
table.errors .rowlink .group-pill { flex: none; }
table.errors td.empty { padding: 40px; text-align: center; color: var(--muted); }
table.errors th.act { width: 80px; }
table.errors td.act { text-align: right; }
table.errors .rowact { color: var(--muted); font-size: 12px; visibility: hidden; }
table.errors tbody tr:hover .rowact, table.errors .rowact:focus-visible { visibility: visible; }
@media (pointer: coarse) { table.errors .rowact { visibility: visible; } }
/* stacked mobile rows: a hover-gated (invisible) action would still claim a
   flex line and leave a blank gap — drop it entirely on fine pointers there
   (the error page carries Resolve); coarse pointers keep the visible button */
@media (max-width: 700px) and (pointer: fine) { table.errors .rowact { display: none; } }
form.inline { display: inline-block; margin-left: 10px; }

/* analytics charts */
/* dashboard graphs: a native disclosure, same show/hide affordance as the
   per-chart "Data as table" (not a button mimicking the bars/lines switch) */
details.graphs { margin-bottom: 20px; }
details.graphs > summary { width: fit-content; margin-bottom: 12px; color: var(--muted); font-size: 12px; cursor: pointer; }
details.graphs > summary:hover { color: var(--ink); }
.chart-card { position: relative; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 16px 20px; margin-bottom: 20px; }
/* one control bar under the title: legend left, view switch right (mobile: switch drops to the left) */
.chart-bar { display: flex; align-items: center; gap: 8px 16px; flex-wrap: wrap; margin-bottom: 12px; }
.chart-controls { margin-left: auto; }
.chart-card h2 { margin: 0 0 10px; }
/* overflow visible: the top gridline's label sits at the viewBox edge, and the
   mobile axis-text bump (below) would otherwise crop its ascenders */
.chart-card svg.chart { display: block; width: 100%; height: auto; overflow: visible; }
.chart-card .gridline { stroke: var(--line); stroke-width: 1; }
.chart-card text.axis { fill: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
/* the chart SVG scales with its 960-unit viewBox, so 11 user units of axis
   text lands near 4px on a phone — the user-unit size steps up as the card
   narrows, each step chosen to keep the rendered text near 11px */
@media (max-width: 700px) { .chart-card text.axis { font-size: 16px; } }
@media (max-width: 520px) { .chart-card text.axis { font-size: 24px; } }
@media (max-width: 420px) { .chart-card text.axis { font-size: 30px; } }
.chart-card .seg:hover { opacity: .82; }
.chart-card .line { fill: none; stroke-width: 2; }
.chart-card .point { stroke: var(--card); stroke-width: 2; } /* surface ring so overlapping points stay distinct */
.chart-card .point-hit { fill: transparent; } /* oversized, invisible: an easy hover target for the point */
.chart-tip { position: absolute; z-index: 30; pointer-events: none; transform: translate(-50%, -130%); background: var(--ink); color: #fff; font-size: 12px; line-height: 1.3; padding: 4px 8px; border-radius: 4px; white-space: nowrap; box-shadow: 0 2px 8px rgba(0, 0, 0, .2); }
.chart-tip[hidden] { display: none; }
.chart-toggle { display: inline-flex; gap: 6px; }
.chart-toggle a { padding: 4px 12px; border: 1px solid var(--line); border-radius: 5px; font-size: 12px; color: var(--muted); text-decoration: none; }
.chart-toggle a:hover { border-color: var(--muted); }
.chart-toggle a.active { color: var(--ink); border-color: var(--muted); font-weight: 600; }
.legend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--ink); }
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-item .swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
/* Where the legend is also the series switch (dashboard occurrences chart), an
   entry is a link that reads exactly like the static legend beside it — the
   padding is cancelled by an equal negative margin so the row is laid out
   identically, it just takes a comfortable tap. A switched-off series keeps its
   slot, greyed AND struck through: the state must not rest on colour alone, and
   the swatch keeps its hue (dimmed) so the entry is still recognisably its
   series. text-decoration goes on the label, not the link — an inline-block
   swatch is an atomic box a line-through would skip anyway. */
a.legend-item { color: inherit; text-decoration: none; padding: 3px 4px; margin: -3px -4px; border-radius: 4px; }
a.legend-item:hover { background: var(--bg); }
a.legend-item:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }
a.legend-item.off { color: var(--muted); }
a.legend-item.off .legend-label { text-decoration: line-through; }
a.legend-item.off .swatch { opacity: .3; }
/* the way back for everything at once — a chip (the payload legend's "Show all"
   recipe), sized down to sit in a 12px legend row without shouting */
.legend-reset { font-size: 12px; padding: 2px 9px; text-decoration: none; }
/* every series switched off: the note replaces the plot, and says which of "no
   data" and "nothing switched on" this is */
p.chart-empty { margin: 0; padding: 40px 0; text-align: center; color: var(--muted); font-size: 13px; }
details.data-table { margin-top: 10px; }
details.data-table summary { color: var(--muted); font-size: 12px; cursor: pointer; }
details.data-table table { margin-top: 8px; }
details.data-table td.num, details.data-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
details.data-table td.num.strong { font-weight: 700; }
/* Occurrence volume in four windows: ONE card divided by hairlines, not four
   cards with gaps between them. Four bordered boxes each holding one number read
   as a widget wall (the thing this app's anti-references are named after), and two
   of them are routinely empty — an error last seen on Tuesday has nothing in the
   last hour. As one strip it is a row of vital signs: same numbers, ~40% of the
   height, and the eye crosses it in one move. */
.vitals { display: grid; grid-template-columns: repeat(4, 1fr); background: var(--card);
          border: 1px solid var(--line); border-radius: 6px; margin: 12px 0 20px; overflow: hidden; }
.vital { position: relative; padding: 10px 14px 12px; border-left: 1px solid var(--line); }
.vital:first-child { border-left: 0; }
.vital-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.vital-title { color: var(--muted); font-size: 13px; }
.vital-total { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
svg.spark { display: block; width: 100%; height: auto; }
svg.spark .seg { fill: var(--ink); }
svg.spark .seg:hover { opacity: .82; }
/* the baseline is what makes an empty window legible: without it the plot area of
   a quiet hour is blank white, which reads as "failed to load" rather than "zero".
   Non-scaling so it stays a hairline whatever width the column takes. */
svg.spark .base { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* the ghost recipe — a link wearing it (Edit, Cancel) must read as the same
   control as the buttons beside it, not as body-copy link text */
button.headact, a.headact, input[type="submit"].headact { background: #fff; color: var(--ink); border: 1px solid var(--line); border-radius: 5px; padding: 4px 12px; font-size: 13px; font-weight: 400; cursor: pointer; }
button.headact:hover, a.headact:hover, input[type="submit"].headact:hover { background: #fff; border-color: var(--muted); }
a.headact { display: inline-block; text-decoration: none; line-height: 1.4; }

/* bulk-selection UI (merge / resolve / reopen / delete) */
.row .rowlink { display: contents; color: inherit; }   /* children join the row's flex layout */
.row input.row-check { accent-color: #1f2430; }
.group-pill { background: #1f2430; color: #fff; }
table.errors thead input#select-all { accent-color: #1f2430; vertical-align: middle; }
.bulk-bar { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
/* the count leads the bar: it says what the buttons would act on */
.bulk-bar .selection-count { color: var(--muted); font-size: 12px; min-width: 8.5rem; }
.bulk-bar .info-btn { margin-left: auto; }
/* Assign is one control in two parts — the person and the verb — so they sit
   tighter to each other than to the actions beside them. */
.bulk-bar .bulk-assign { display: flex; align-items: center; gap: 6px; }
.bulk-bar .bulk-assign select {
  height: 33px; padding: 0 8px; border: 1px solid var(--line); border-radius: 6px;
  background: #fff; color: var(--ink); font: inherit; font-size: 13px; cursor: pointer;
}
.bulk-bar .bulk-assign select:disabled { opacity: .45; cursor: not-allowed; }
.bulk-bar .bulk-assign select:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }
/* an action that cannot apply to the current selection recedes rather than
   disappearing — the row of actions keeps its shape while you tick */
.bulk-bar button:disabled { opacity: .45; cursor: not-allowed; }
.bulk-bar button:disabled:hover { border-color: var(--line); background: #fff; }
.bulk-bar button.merge-btn:disabled:hover { background: #1f2430; }
/* the primary recipe — buttons, submits, and links that ARE the page's one
   commanding action (e.g. "New group") share it exactly */
button.merge-btn, input[type="submit"], a.primary-action { background: #1f2430; color: #fff; border: 0; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
button.merge-btn:hover, input[type="submit"]:hover, a.primary-action:hover { background: #343c4d; }
a.primary-action { display: inline-block; color: #fff; text-decoration: none; }
.field { margin-bottom: 14px; max-width: 640px; }
.field label { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 4px; }
.field input[type="text"], form.new-project input[type="text"] { width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 14px; }
.field input.mono, .mono, ul.preview li { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
/* long call-site paths wrap instead of forcing the grouped-errors table wide —
   and the count columns shrink to their numbers first, so a call site only
   breaks once the line is genuinely out of room */
td.call-site { overflow-wrap: anywhere; }
td.call-site .cs-method { display: inline-block; white-space: nowrap; }
table.members th.num, table.members td.num { width: 1%; white-space: nowrap; }
ul.preview { margin: 8px 0 0 18px; }
ul.preview li { padding: 2px 0; }
/* ===== error-groups admin table — speaks the errors-table dialect =====
   Titles are payload evidence (Mono Evidence Rule): mono, quiet ink, hover
   underline. Patterns clamp to a two-line glimpse (full rule on the group's
   page / on hover); numbers right-align, zero counts stay muted; Dissolve
   reveals on row hover like the errors table's row actions. */
table.groups td { vertical-align: top; }
table.groups th.num, table.groups td.num { text-align: right; white-space: nowrap; }
table.groups td.num, table.groups td.meta { color: var(--muted); font-size: 12px; }
table.groups td.num.strong { color: var(--ink); font-weight: 700; font-size: 13px; }
table.groups td:first-child { min-width: 260px; }
table.groups .grouplink { color: var(--ink); text-decoration: none; font-weight: 400; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
table.groups .grouplink:hover { text-decoration: underline; text-underline-offset: 2px; }
table.groups .grouplink:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; border-radius: 3px; }
table.groups td.pattern code { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
table.groups tbody tr:hover td { background: #fafbfc; }
table.groups td.empty { padding: 40px; text-align: center; color: var(--muted); }
table.groups .dissolve { visibility: hidden; }
table.groups tbody tr:hover .dissolve, table.groups .dissolve:focus-visible { visibility: visible; }
@media (pointer: coarse) { table.groups .dissolve { visibility: visible; } }

/* ===== notification channels =====
   Same dialect as the groups admin: quiet table, mono for the machine-readable
   destination, muted metadata, actions on the right. A silenced channel is
   greyed, but its state is also spelled out in the "On" column — never colour
   or dimming alone. */
table.channels td { vertical-align: top; }
table.channels th.num, table.channels td.num { text-align: right; white-space: nowrap; }
table.channels td.meta { color: var(--muted); font-size: 12px; }
table.channels td.name { min-width: 240px; }
/* 12px, not the badge's 11px: this is metadata to read, and DESIGN.md floors
   readable meta text at 12px (11px is reserved for filled badges) */
table.channels td.name .meta { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
/* the destination is evidence someone will copy or compare — mono, clamped to
   two lines so a signed webhook URL can't stretch the table */
table.channels td.name .target {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px; color: var(--ink); overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
table.channels tbody tr:hover td { background: #fafbfc; }
table.channels td.empty { padding: 40px; text-align: center; color: var(--muted); }
table.channels tr.silenced td.name .target, table.channels tr.silenced td.meta { color: var(--muted); }
table.channels .delivery-failed { color: var(--error); font-weight: 700; }
/* a failure reason is worth reading, not glancing at — ink, and clamped */
table.channels .detail {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  color: var(--ink); overflow-wrap: anywhere;
}
table.channels td.act { white-space: nowrap; }
table.channels td.act > * { margin-left: 6px; display: inline-block; }
table.channels td.act > *:first-child { margin-left: 0; }
table.channels .toggle-form { display: inline; }
table.channels .toggle-form input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: #1f2430; }

/* channel form: the same input recipe as .field's text inputs, extended to the
   controls a channel needs (selects, numbers, the payload textarea) */
.field textarea, .field select, .field input[type="number"] {
  width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px;
  font-size: 14px; background: var(--card); color: var(--ink);
}
.field textarea.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; line-height: 1.5; }
.field input[type="number"] { max-width: 8rem; }
.field p.muted { margin: 4px 0 0; font-size: 12px; }
/* checkbox groups (events, instances): a fieldset so the legend names the set
   for screen readers instead of a bare label column */
fieldset.field-group { margin: 0 0 14px; padding: 0; border: 0; max-width: 640px; }
fieldset.field-group legend {
  padding: 0; font-size: 12px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); margin-bottom: 6px;
}
label.check { display: flex; align-items: baseline; gap: 8px; padding: 3px 0; font-size: 14px; cursor: pointer; }
label.check input[type="checkbox"] { width: 16px; height: 16px; accent-color: #1f2430; cursor: pointer; }
label.check .muted { color: var(--muted); font-size: 12px; }
/* the global p.muted pulls itself up by 8px (it sits under page headings there);
   inside a checkbox group it must breathe below the last option instead */
fieldset.field-group p.muted { margin: 8px 0 0; font-size: 12px; }
/* step 1 of authoring a channel: two plain cards, standard link affordance —
   no radios, no wizard chrome for a two-way choice */
.kind-choice { display: flex; flex-wrap: wrap; gap: 12px; max-width: 640px; margin: 0 0 8px; }
.kind-card {
  flex: 1 1 260px; padding: 16px 18px; background: var(--card); border: 1px solid var(--line);
  border-radius: 6px; text-decoration: none; color: var(--ink);
}
.kind-card:hover { border-color: var(--muted); background: #fafbfc; }
/* a card's own heading is ink and sentence-case — the global h2 is the section
   "eyebrow" recipe (uppercase fog), which is not what this is */
.kind-card h2 { margin: 0 0 4px; font-size: 15px; font-weight: 700; color: var(--ink); text-transform: none; letter-spacing: normal; }
.kind-card p { margin: 0; font-size: 13px; color: var(--muted); }

/* the channel form's three sections — where it goes, what wakes it, how often */
.form-section { max-width: 640px; margin: 0 0 26px; }
.form-section h2 {
  margin: 0 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--line);
  font-size: 15px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
}
/* the plumbing most channels never need, folded away by default */
details.advanced { margin: 0 0 14px; }
details.advanced > summary {
  display: inline-block; font-size: 13px; color: var(--link); cursor: pointer; padding: 2px 0;
}
details.advanced[open] > summary { margin-bottom: 10px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.form-actions { display: flex; align-items: center; gap: 10px; margin-top: 20px; }
.nowrap { white-space: nowrap; }
code.placeholder {
  display: inline-block; margin: 2px 4px 0 0; padding: 1px 6px; border: 1px solid var(--line);
  border-radius: 3px; background: var(--card); font-size: 12px; color: var(--ink);
}

/* ===== item page: the stored occurrence feed — when, what was hit, who =====
   The request is payload evidence (mono, via .mono) and the flexible absorber:
   free horizontal space widens it, overflow ellipsizes (full URL in the
   tooltip). Stamps/ages/users are glanceable meta — fog, never wrapping. */
table.occurrences th.stamp, table.occurrences th.ago { white-space: nowrap; }
table.occurrences td.meta { color: var(--muted); font-size: 12px; white-space: nowrap; }
table.occurrences td.req { width: 100%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
table.occurrences td.who { color: var(--muted); font-size: 12px; white-space: nowrap; max-width: 24ch; overflow: hidden; text-overflow: ellipsis; }
table.occurrences tbody tr:hover td { background: #fafbfc; }
table.occurrences td.empty { padding: 40px; text-align: center; color: var(--muted); }

button.copy { background: #fff; border: 1px solid var(--line); border-radius: 5px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
/* copy buttons inside section headers keep body type, not the h2's caps */

/* Danger-ghost (DESIGN.md §Buttons): white, hairline border, error-red text —
   destructive actions never fill red at rest. A component, not a table detail:
   scoped to `table button.dissolve` it fell back to the browser's native button
   next to a .headact in a .head-actions row. Geometry matches .headact so the two
   sit level; tables keep the denser variant below. */
button.dissolve { background: #fff; border: 1px solid var(--line); border-radius: 5px; padding: 4px 12px; font-size: 13px; color: var(--error); cursor: pointer; }
button.dissolve:hover { border-color: var(--error); }
table button.dissolve { padding: 4px 10px; font-size: 12px; }

/* projects admin */
/* inline per-row form: input + Save at table-row scale (the global
   input[type=submit] pill is bulk-bar sized and dwarfs the row) */
/* integrations cell: one quiet reference line (JIRA key, group/repo, webhook
   tick), the labelled form waiting inside the Edit disclosure's popover */
.int-summary { display: flex; align-items: center; gap: 14px; white-space: nowrap; }
.int-ref { display: inline-flex; align-items: baseline; gap: 6px; }
.int-ref .int-kind { color: var(--muted); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
.int-ref code { font-size: 13px; color: var(--ink); }
.int-ref .hook-ok { color: var(--muted); font-size: 12px; cursor: default; }
.int-none { color: var(--muted); }
td.created { white-space: nowrap; } /* a date split across lines reads as two numbers */
form.integrations { display: grid; grid-template-columns: auto 1fr; gap: 6px 8px; align-items: center; margin: 0; min-width: 300px; }
form.integrations label { display: contents; }
form.integrations label span { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
form.integrations input[type="text"] { width: 100%; padding: 4px 8px; border: 1px solid var(--line); border-radius: 5px; font-size: 13px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
form.integrations input.jira { text-transform: uppercase; } /* JIRA keys only — never the URL */
form.integrations input[type="submit"] { grid-column: 2; justify-self: start; background: #fff; color: var(--ink); border: 1px solid var(--line); border-radius: 5px; padding: 4px 12px; font-size: 12px; font-weight: 400; }
form.integrations input[type="submit"]:hover { border-color: var(--muted); }
/* close→resolve webhook state under the repo field: a healthy hook is a quiet
   tick, a missing one names its reason beside the Install retry */
form.new-project { display: flex; gap: 10px; margin-bottom: 20px; max-width: 480px; }
form.new-project input[type="text"] { flex: 1; }
/* chart-color swatch: the native color input styled as the legend's dot; for
   members (read-only) a plain span wears the same size. Saves itself onchange. */
td.name { white-space: nowrap; }
td.name form.color-form { display: inline-flex; align-items: center; gap: 8px; margin: 0; vertical-align: middle; }
td.name input[type="color"] { inline-size: 18px; block-size: 18px; padding: 0; border: 1px solid var(--line); border-radius: 50%; background: none; cursor: pointer; }
td.name input[type="color"]:hover { border-color: var(--muted); }
td.name input[type="color"]:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
td.name input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
td.name input[type="color"]::-webkit-color-swatch { border: none; border-radius: 50%; }
td.name input[type="color"]::-moz-color-swatch { border: none; border-radius: 50%; }
/* "2 channels" / "none" — a way in and a status in one cell; the zero case is
   fog because "nobody is told" is metadata, not an alarm */
td.alerts { white-space: nowrap; }
td.alerts a { font-size: 13px; }
td.alerts a.quiet { color: var(--muted); }
td.name .swatch { display: inline-block; inline-size: 18px; block-size: 18px; border: 1px solid var(--line); border-radius: 50%; margin-right: 8px; vertical-align: middle; }
/* drag & drop ordering: the handle stays quiet ink until hovered/focused; the
   row being dragged dims in place while the browser's drag image travels */
th.handle-col, td.handle { width: 1%; padding-right: 0; }
button.drag-handle { cursor: grab; background: none; border: none; padding: 2px 6px; color: var(--muted); font-size: 13px; }
button.drag-handle:hover, button.drag-handle:focus-visible { color: var(--ink); }
tr.dragging td { opacity: .4; }
td.token { white-space: nowrap; }
td.token code { font-size: 12px; background: #eef1f4; padding: 3px 7px; border-radius: 4px; }
td.token button.copy { margin-left: 8px; color: var(--ink); }
td.token button.copy:hover { border-color: var(--muted); }
tr.highlight td { background: var(--ok-bg); }

table { width: 100%; border-collapse: collapse; background: var(--card); border: 1px solid var(--line); border-radius: 6px; }
th, td { text-align: left; padding: 10px 16px; border-bottom: 1px solid var(--line); font-size: 13px; }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; }
tr:last-child td { border-bottom: 0; }

pre { background: #1f2430; color: var(--wire-bright); padding: 16px; border-radius: 6px; overflow: auto; font-size: 12px; }
/* stack trace + request data on the error page: bounded, scroll instead of growing */
pre.stacktrace { max-height: 420px; font-size: 13px; line-height: 1.6; }

/* One occurrences-table row's own payload, behind a disclosure and fetched lazily.
   Same metrics the "why this fingerprint?" panel had here, so the row is unchanged
   by that panel's removal — only what opens is different (the marked payload now,
   which says in place what the panel said by path). */
details.rowpayload { margin: -8px 0 20px; }
details.rowpayload > summary { width: fit-content; color: var(--muted); font-size: 12px; cursor: pointer; }
details.rowpayload > summary:hover { color: var(--ink); }
details.rowpayload[open] > summary { margin-bottom: 10px; }

/* The payload legend: one row of states, each naming a treatment used below it. */
p.legend { margin: 0; display: flex; flex-wrap: wrap; gap: 16px; color: var(--muted); font-size: 12px; }
p.legend span { display: inline-flex; align-items: baseline; gap: 5px; }

/* The raw payload marks its own grouping inputs, always on: what fed the key is lit
   and bold, environment is set apart (it groups — on the Item, not in the key), and
   the rest dims. Not a toggle: there is no reading of a payload where you would
   rather not know which of it decided the bucket, and off-by-default meant nobody
   ever saw the marks. Only weight and colour are added — the text stays the wire
   verbatim, so a copy-paste is still evidence.
   Key Cyan (#67e8f9) is the single hue outside the severity scale, granted to this
   one surface (DESIGN.md § Colors): the payload block needs a mark that survives
   being read at 12px inside a wall of JSON, which weight alone did not do. It is
   nowhere near the five level colours, so nothing here can read as a severity.
   Contrast on #1f2430: dim 5.1:1, lit 10.7:1 — the dim state is the payload's
   permanent voice now, so it has to clear AA on its own, not merely differ. */
pre.payload { color: var(--wire-dim); }
pre.payload .fp { color: var(--key-cyan); font-weight: 700; background: var(--key-cyan-tint); border-radius: 3px; }
pre.payload .fp-key { color: var(--wire-lifted); font-weight: 700; }
/* Struck inside a lit value: this much of it was folded to a placeholder on the way
   into the key, so it is the one part that does NOT discriminate. Still cyan and
   bold — it is inside the marked value — but line-through and dropped to the dim
   step, which is the "counts for nothing" voice used everywhere else. */
pre.payload .fp del { color: var(--wire-dim); text-decoration: line-through; text-decoration-thickness: 1px; }
/* environment: bright enough to escape the dim rest, dotted rather than coloured —
   it is a third state, not a second key */
pre.payload .fp-aside { color: var(--wire-bright); border-bottom: 1px dotted var(--wire-dim); }
pre.payload .fp-aside-key { color: var(--wire-lifted); }
/* the legend's swatches quote those three states exactly */
.payload-legend { margin: -2px 0 8px; }
.payload-legend .mark.fed { color: var(--key-cyan-ink); }
.payload-legend .mark.aside-mark { color: var(--ink); }
.payload-legend .mark.dim { color: var(--muted); }
/* this entry demonstrates itself — the struck words are the swatch */
.payload-legend del { color: var(--muted); text-decoration-thickness: 1px; }
/* Show all / Hide ignored sits at the end of the legend, after the states it governs. */
.payload-legend .payload-toggle { margin-left: auto; }
/* A folded-away run. Italic, because it is not payload text at all — it is us saying what
   we left out, and it must not be mistakable for a value. Italics and the ellipses carry
   that on their own: dropping the colour below --wire-dim would have put text you are
   meant to READ under 4.5:1 (3.4:1 as first written), and a count you cannot read is
   worse than no count. */
pre.payload .fold-gap { color: var(--wire-dim); font-style: italic; }

/* The resulting key, stated on one line above the payload that produced it. A SHA1
   wraps rather than truncates: it is the thing you paste into a URL or an MCP call,
   so all forty characters have to be selectable. */
p.fingerprint-line { margin: 0 0 6px; }
p.fingerprint-line .mono { margin-left: 6px; color: var(--ink); overflow-wrap: anywhere; }
.kv-card { border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.kv-card table { border: 0; }
table.kv th { width: 220px; color: var(--muted); font-weight: 600; text-transform: none; vertical-align: top; }
table.kv td { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; word-break: break-all; }
table.kv tr.section th { background: var(--bg); text-transform: uppercase; letter-spacing: .04em; }

/* auth */
.topbar .user { margin-left: auto; display: flex; align-items: center; gap: 10px; color: #b9c2cf; font-size: 13px; }
.topbar .user form.logout, .topbar .user form.startpage { margin: 0; }
.topbar .user form.startpage button { padding: 4px 8px; font-size: 14px; line-height: 1; }
.topbar .user button { background: #2c3340; color: #b9c2cf; border: 1px solid #3a4250; border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 12px; }
.topbar .user button:hover { color: #fff; }

/* Home page only — a nod to the GM design system (gomus/style-guide):
   "Museum Tech on the deep green base". Values borrowed from its tokens:
   deep green base --gm-pdk #04342c, brand primary --gm-p #0a7d61, warm stone
   text --gm-g7 #44403c, radius-lg 12px / radius-xl 16px. Deliberately scoped
   to .login; the rest of the app keeps its own palette. */
/* Home page: the body is a viewport-sized flex column (hero / footer — the
   topbar doesn't render signed-out) — the hero takes exactly the leftover
   space, so the page never scrolls without need — and the body itself is
   deep green, so the app-bg white can never peek out around the hero or
   behind the footer. overflow-x absorbs the full-bleed breakout's
   scrollbar-width overshoot. */
body:has(.login) { background: #04342c; overflow-x: hidden; display: flex; flex-direction: column; min-height: 100vh; }
/* width: 100% — as flex items with auto side margins, the containers would
   otherwise shrink-to-fit instead of stretching (auto margins beat stretch) */
body:has(.login) main.container { flex: 1; display: flex; flex-direction: column; margin-top: 0; margin-bottom: 0; width: 100%; }
/* the footer sits on the green — no top gap, hairline and text lightened */
body:has(.login) footer.container { margin-top: 0; margin-bottom: 0; width: 100%; }
body:has(.login) .footnav { margin-top: 0; border-top-color: rgba(255, 255, 255, .18); }
body:has(.login) .footnav .colophon { color: rgba(255, 255, 255, .55); }
/* Full-bleed: the hero escapes the centered .container to the viewport edges
   (negative-margin breakout), squared corners. */
.login { flex: 1; display: flex; align-items: center; justify-content: center; background: #04342c; margin: 0 calc(50% - 50vw); position: relative; overflow: hidden; }
/* the constellation draws behind the panel and never intercepts the form */
.login .constellation { position: absolute; inset: 0; pointer-events: none; }
.login .panel { position: relative; z-index: 1; background: var(--card); border: 0; border-radius: 12px; padding: 40px; text-align: center; max-width: 340px; box-shadow: 0 12px 32px rgba(2, 35, 28, .35); }
.login .panel h1 { margin: 0 0 24px; color: #04342c; }
.login .google-btn button, .login form button { display: inline-flex; align-items: center; gap: 4px; background: #fff; color: #3c4043; border: 1px solid #dadce0; border-radius: 8px; padding: 10px 18px; font-size: 14px; font-weight: 500; cursor: pointer; }
.login form button:hover { background: #f8f9fa; box-shadow: 0 1px 2px rgba(0,0,0,.1); }
.login .remember { display: inline-flex; align-items: center; gap: 6px; margin: 16px 0 0; color: #44403c; font-size: 13px; cursor: pointer; }
.login .remember input { accent-color: #0a7d61; }

/* fixed full-width bar pinned to the bottom — never shifts page layout; auto-dismissing */
.flash-region { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; display: flex; flex-direction: column; }
.flash { padding: 12px 16px; text-align: center; font-size: 14px; font-weight: 600; color: #fff; cursor: pointer; animation: flash-in 260ms cubic-bezier(0.22, 1, 0.36, 1); }
.flash.notice { background: #256029; } /* white text clears AA (≈6.9:1) */
.flash.alert { background: #a02622; } /* distinct from severity reds (Severity Monopoly); ≈6.5:1 */
@keyframes flash-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .flash { animation: none; } }

/* labels and dropdown toggles are UI chrome, not content — never selectable
   (a double-click near them must not highlight "INSTANCE" etc.) */
.filter-label, summary, form.integrations label span { user-select: none; }

/* filter rows (instance, level, search) share one label column so chips align */
.filter { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.filter .filter-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; width: var(--filter-label-w); flex-shrink: 0; }
.chip { display: inline-block; padding: 3px 11px; border-radius: 13px; font-size: 13px; background: var(--card); border: 1px solid var(--line); color: var(--ink); }
.chip:hover { border-color: var(--muted); }
.chip.active { background: #1f2430; color: #fff; border-color: #1f2430; }
/* Desktop: chips flow inline in the .filter row; the mobile dropdown is hidden.
   The Sort filter and per-row meta only exist for the stacked mobile layout. */
.level-chips { display: contents; }
.level-menu, .filter.mobile-sort, .row-meta { display: none; }

/* multi-select filter dropdown (instances) — light disclosure, checkbox rows */
.filter-dropdown { position: relative; display: inline-block; }
.filter-dropdown > summary {
  list-style: none; cursor: pointer;
  background: var(--card) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237f8c8d' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 12px center;
  border: 1px solid var(--line); border-radius: 6px; padding: 6px 34px 6px 12px; font-size: 13px; color: var(--ink);
}
.filter-dropdown > summary::-webkit-details-marker { display: none; }
.filter-dropdown[open] > summary { border-color: var(--muted); }
.filter-dropdown .menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; min-width: 200px; max-height: 320px; overflow: auto;
  background: var(--card); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18); padding: 4px; display: flex; flex-direction: column;
}
.filter-dropdown .menu .chip { border: 0; border-radius: 6px; background: transparent; color: var(--ink); text-align: left; padding: 8px 12px; font-size: 14px; }
.filter-dropdown .menu .chip:hover { background: #f0f2f5; }
/* instance rows read as checkboxes; the "All instances" reset (first) doesn't */
.filter-dropdown .menu .chip:not(:first-child)::before { content: "☐"; margin-right: 8px; }
.filter-dropdown .menu .chip:not(:first-child).active::before { content: "☑"; }
.filter-dropdown .menu .chip.active { background: #f0f2f5; }
p.muted { color: var(--muted); margin: -8px 0 16px; font-size: 13px; }
/* the stretch between a heading and its table (descriptions, filters) shares
   the tables' inner text inset, so the left reading edge runs continuous —
   headings stay on the container edge; carded/inline contexts opt out */
p.muted, .filters { padding-inline: 16px; }
.panel p.muted, form p.muted { padding-inline: 0; }

/* footer nav: the reference/admin destinations, quiet below the work surface */
.footnav { display: flex; align-items: baseline; margin-top: 48px; padding-top: 14px; padding-bottom: 20px; border-top: 1px solid var(--line); }
.footnav nav { display: flex; flex-wrap: wrap; gap: 8px 28px; }
/* each group is "<scope label> link link" — the label says what the links are
   scoped to, so a per-project admin page can't be mistaken for a global one */
.footnav .nav-group { display: flex; align-items: baseline; gap: 14px; }
.footnav .nav-scope { color: var(--muted); font-size: 12px; }
.footnav .colophon { margin-left: auto; color: var(--muted); font-size: 12px; }
.footnav a { color: var(--muted); font-size: 13px; text-decoration: none; }
.footnav a:hover { color: var(--ink); }
.footnav a.current { color: var(--ink); font-weight: 600; }

/* ===== Page help (info button + popover) — shared/_info_button =====
   The ⓘ sits at the right edge of its heading line; the panel is a native
   popover (centered, light-dismiss), so it earns the Float shadow. */
h1:has(.info-btn) { display: flex; align-items: center; gap: 10px; }
/* perfectly round, exactly the h1 line height (20px × 1.2) */
.info-btn {
  margin-left: auto; flex-shrink: 0; aspect-ratio: 1;
  width: 24px; border-radius: 50%;
  border: 1px solid var(--line); background: var(--card); color: var(--muted);
  font: 700 14px/1 -apple-system, "Segoe UI", Roboto, sans-serif;
  cursor: pointer; padding: 0;
}
.info-btn:hover { border-color: var(--muted); color: var(--ink); }
.info-btn:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
/* the panel lives inside the h1, so the font shorthand resets its type */
.help-popover {
  max-width: min(440px, calc(100vw - 48px));
  background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 8px;
  padding: 14px 18px; text-align: left;
  font: 400 14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; letter-spacing: normal;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
}
.help-popover p { margin: 0 0 8px; }
.help-popover p:last-child { margin-bottom: 0; }
.help-popover::backdrop { background: rgba(31, 36, 48, .25); backdrop-filter: blur(1px); }
@media (prefers-reduced-motion: no-preference) {
  .help-popover:popover-open { animation: help-in 150ms ease-out; }
  @keyframes help-in { from { opacity: 0; transform: translateY(4px); } }
}

/* MCP docs: token-care callout — quiet bordered card; the bold lead carries the emphasis */
.warn { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 12px 16px; margin: 12px 0; font-size: 13px; }
.warn code { background: #eef1f4; padding: 2px 6px; border-radius: 4px; }

/* glossary: the second name a term also goes by ("Instance (environment)"). It never had a
   rule, because until now it never rendered at all — Slim reads `span.muted (environment)` as
   an attribute wrapper and dropped the text, so every one of these was silently empty. */
td .muted { color: var(--muted); font-size: 12px; font-weight: 400; }

/* glossary: entity-relations diagram — scales to the column, never overflows */
.diagram { margin: 4px 0 20px; }
.diagram svg { display: block; width: 100%; max-width: 640px; height: auto; }

@media (max-width: 700px) {
  /* error header stacks: title, then actions */
  /* stacked: children keep their natural left edge (center would middle them) */
  .error-head { flex-direction: column; align-items: flex-start; gap: 8px; }

  /* the vitals strip folds to 2×2 rather than four full-width bands: stacked, the
     two quiet windows of an old error pushed the diagnosis a screen down. Dividers
     follow the fold — the left rule belongs to the right-hand column only, and the
     second row gains a top one. */
  .vitals { grid-template-columns: repeat(2, 1fr); }
  .vital:nth-child(odd) { border-left: 0; }
  .vital:nth-child(n+3) { border-top: 1px solid var(--line); }

  /* the facts group takes the full row, so the chips start at the left edge below
     it instead of hanging off the end of "Total 9" */
  .summary { gap: 8px 16px; }
  .summary .facts { flex: 1 0 100%; }

  /* on the chart control bar, the switch drops to the left under the legend */
  .chart-controls { margin-left: 0; }

  /* Level: swap the wrapping chip row for a compact dropdown (project-switcher
     look, light theme). Each tap navigates → the menu closes and reflects the
     new selection in its summary. The Sort menu (thead's replacement in the
     stacked layout) shares the same anatomy.
     ponytail: reload-per-toggle, not a live multi-check — needs no JS and reuses
     the chip links; add a frame-targeted auto-submit form only if it grates. */
  .level-chips { display: none; }
  .filter.mobile-sort { display: flex; }
  .level-menu, .sort-menu { display: inline-block; position: relative; }
  .level-menu summary, .sort-menu summary {
    list-style: none; cursor: pointer;
    background: var(--card) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%237f8c8d' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 12px center;
    border: 1px solid var(--line); border-radius: 6px;
    padding: 8px 34px 8px 12px; font-size: 14px; color: var(--ink);
  }
  .level-menu summary::-webkit-details-marker, .sort-menu summary::-webkit-details-marker { display: none; }
  .level-menu[open] summary, .sort-menu[open] summary { border-color: var(--muted); }
  .level-menu .menu, .sort-menu .menu {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; min-width: 200px;
    max-width: calc(100vw - 28px);
    background: var(--card); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .18); padding: 4px; display: flex; flex-direction: column;
  }
  .level-menu .menu .chip, .sort-menu .menu .chip {
    border: 0; border-radius: 6px; background: transparent; color: var(--ink);
    text-align: left; padding: 11px 12px; font-size: 14px;
  }
  .level-menu .menu .chip:hover, .sort-menu .menu .chip:hover { background: #f0f2f5; }
  /* Sort is single-choice: the active row carries a check, no boxes */
  .sort-menu .menu .chip.active { background: #f0f2f5; }
  .sort-menu .menu .chip.active::before { content: "✓ "; }
  /* Level rows read as checkboxes; the neutral fill keeps the tick legible
     (the inline chips' level-colour fill would hide it). */
  .level-menu .menu .chip.level-critical, .level-menu .menu .chip.level-error,
  .level-menu .menu .chip.level-warning, .level-menu .menu .chip.level-info,
  .level-menu .menu .chip.level-debug { text-transform: capitalize; }
  .level-menu .menu .chip[class*="level-"]::before { content: "☐"; margin-right: 8px; }
  .level-menu .menu .chip[class*="level-"].active { background: #f0f2f5; }
  .level-menu .menu .chip[class*="level-"].active::before { content: "☑"; }

  /* Errors tables restack: each row becomes a small stanza — badges first,
     the title at full width, then a quiet meta line (.row-meta replaces the
     hidden numeric columns; the Sort filter replaces the hidden thead). No
     sideways pan: the title is the answer and must be on screen. */
  /* white-space counters the generic mobile `table { white-space: nowrap }` */
  table.errors { display: block; min-width: 0; white-space: normal; }
  table.errors thead { display: none; }
  table.errors tbody { display: block; }
  table.errors tbody tr {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
    padding: 10px 12px 12px 8px; border-bottom: 1px solid var(--line);
    border-left: 4px solid transparent;
  }
  table.errors tbody tr:last-child { border-bottom: 0; }
  /* the severity spine moves from the first cell to the row itself */
  table.errors tr.error td:first-child, table.errors tr.critical td:first-child,
  table.errors tr.warning td:first-child, table.errors tr.info td:first-child,
  table.errors tr.debug td:first-child, table.errors td:first-child { border-left: 0; }
  table.errors tr.error, table.errors tr.critical { border-left-color: var(--error); }
  table.errors tr.warning { border-left-color: var(--warning); }
  table.errors tr.info { border-left-color: var(--info); }
  table.errors tr.debug { border-left-color: var(--debug); }
  table.errors td { display: block; border-bottom: 0; padding: 0; }
  table.errors td.num, table.errors td.ago { display: none; } /* .row-meta carries these */
  /* the assignee mark joins the badges on the stacked row — but an unassigned row
     must not pay a flex gap for a cell with nothing in it */
  table.errors td.who:empty { display: none; }
  table.errors td.proj, table.errors td.act { order: 3; margin-left: auto; }
  /* a small screen may wrap the name again — space is real here */
  table.errors td.proj { color: var(--muted); font-size: 12px; white-space: normal; max-width: none; }
  table.errors td.act { text-align: right; }
  table.errors .rowact { padding: 6px 0 6px 10px; }
  table.errors td.title-cell { order: 4; flex-basis: 100%; width: auto; max-width: none; overflow: visible; }
  table.errors .rowlink { flex-wrap: wrap; gap: 4px 10px; }
  table.errors .rowlink .title { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; }
  table.errors .row-meta { display: block; margin-top: 4px; color: var(--muted); font-size: 12px; }
  table.errors input.row-check, table.errors input#select-all { width: 18px; height: 18px; }
  /* per-cell hover paint looks patchy on flex rows; hover the row instead */
  table.errors tbody tr:hover td { background: transparent; }
  table.errors tbody tr:hover { background: #fafbfc; }
  /* full-width special rows: deploy markers, empty state, load-more sentinel */
  table.errors tr.deploy-row { padding: 0; border-left: 0; }
  table.errors tr.deploy-row td, table.errors td.empty, table.errors td.load-more { flex-basis: 100%; width: 100%; }
  table.errors td.empty { padding: 32px 16px; }
  table.errors td.load-more { padding: 14px; }

  /* Reappeared marker: the desktop version draws a dotted box across the row's
     cells and pins the flag top-right of td.ago. On mobile the row is a flex
     stanza and td.ago is hidden — so the per-cell borders become stray internal
     lines and the flag vanishes. Redraw the box on the row itself (keeping the
     severity spine on the left) and float the flag up as a full-width banner
     line, un-hiding just it from the collapsed ago cell. */
  table.errors tbody tr.reappeared td { border-top: 0; border-bottom: 0; padding-top: 0; }
  table.errors tbody tr.reappeared td:last-child { border-right: 0; }
  /* tbody in the selector outranks the tr:last-child { border-bottom: 0 } rule
     above, so a lone reappeared row keeps its full box. */
  table.errors tbody tr.reappeared {
    border-top: 1px dotted var(--error);
    border-right: 1px dotted var(--error);
    border-bottom: 1px dotted var(--error);
  }
  table.errors tbody tr.reappeared td.ago { display: block; order: -1; flex-basis: 100%; font-size: 0; }
  table.errors tbody tr.reappeared td.ago .reappeared-flag { position: static; font-size: 10px; }

  /* state badges go icon-only: the filling circle carries the progress, the
     full label stays as aria-label + title */
  .badge.state .label { display: none; }
  .badge.state .icon { display: inline; font-size: 13px; line-height: 1; }
  .badge.state { padding: 3px 6px; }

  /* dashboard stat tiles: one flat line, not a wall — the error list is what
     a phone visit is for. The project count is desktop-only chrome (the
     project switcher already answers it). */
  .stat-row { margin-bottom: 14px; }
  .stat-card { min-width: 0; padding: 7px 12px; }
  .stat-card.projects { display: none; }
  .stat-num { font-size: 18px; }
  /* 11px is the label floor (DESIGN.md) — 10px undercut it */
  .stat-label { font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* touch targets & iOS: chips grow; the search field hits 16px so focusing
     it doesn't zoom the page */
  .chip { padding: 8px 14px; border-radius: 18px; }
  .filters .search input { font-size: 16px; }
  .filter-dropdown .menu .chip { padding: 11px 12px; }
  .filter-dropdown .menu { max-width: calc(100vw - 28px); }

  .topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    flex-wrap: wrap;
    gap: 12px;
    height: auto;
    min-height: 52px;
    padding: 8px 14px;
  }

  .topbar .brand {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* small screens: the monkey alone is the home button */
  .topbar .brand .brand-name {
    display: none;
  }

  .topbar .project-switcher {
    order: 3;
    width: 100%;
  }

  .topbar .project-switcher summary {
    width: 100%;
  }

  .topbar .project-switcher .menu {
    width: 100%;
  }

  .topbar .desktop-nav {
    display: none;
  }

  .topbar > a.dash {
    display: none; /* lives in the hamburger panel instead */
  }

  .topbar .mobile-menu {
    display: block;
    position: relative;
    margin-left: auto;
  }

  .topbar .mobile-menu summary {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 38px;
    height: 36px;
    list-style: none;
    border: 1px solid #3a4250;
    border-radius: 6px;
    background: #2c3340;
    cursor: pointer;
    padding: 0 10px;
  }

  .topbar .mobile-menu summary::-webkit-details-marker {
    display: none;
  }

  .topbar .mobile-menu summary span {
    display: block;
    width: 100%;
    height: 2px;
    border-radius: 2px;
    background: #b9c2cf;
  }

  .topbar .mobile-menu summary:hover span,
  .topbar .mobile-menu[open] summary span {
    background: #fff;
  }

  .topbar .mobile-panel {
    display: none;
  }

  .topbar .mobile-menu[open] .mobile-panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 40; /* above the full-width project-switcher row it overlaps */
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-width: min(280px, calc(100vw - 28px));
    padding: 12px;
    background: #2c3340;
    border: 1px solid #3a4250;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .4);
  }

  .topbar nav {
    flex-direction: column;
    gap: 0;
  }

  .topbar nav a {
    padding: 9px 8px;
    border-radius: 5px;
  }

  .topbar nav a:hover {
    background: #3a4250;
  }

  .topbar .user {
    margin-left: 0;
    padding-top: 10px;
    border-top: 1px solid #3a4250;
    align-items: stretch;
    flex-direction: column;
    gap: 8px;
    overflow-wrap: anywhere;
  }

  .topbar .user button {
    width: 100%;
    padding: 8px 10px;
  }

  .container {
    margin: 16px auto;
    padding: 0 14px;
  }

  h1 {
    font-size: 18px;
  }

  .row {
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 8px;
    padding: 12px;
  }

  .row .title {
    flex-basis: 100%;
    order: 2;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .row .meta,
  .count {
    order: 3;
  }

  .bulk-bar {
    align-items: stretch;
    flex-direction: column;
  }
  /* stacked, every action spans the bar — so the Assign pair does too, with the
     person picker taking the slack and the verb keeping its own width */
  .bulk-bar .bulk-assign select { flex: 1; min-width: 0; }

  table {
    display: block;
    overflow-x: auto;
    white-space: nowrap;
  }

  th,
  td {
    padding: 9px 12px;
  }

  /* keep the reading edge continuous at the tighter mobile insets */
  p.muted, .filters { padding-inline: 12px; }
  table.deploys caption { padding: 0 12px 8px; }
}
