/* The board's own rules. Everything below sits on top of Primer, which arrives
 * as four stylesheets loaded before this one (see index.html and
 * internal/server/primer-refresh.sh):
 *
 *   primitives.css   the scales — type, space, radius, borders, motion
 *   theme-light.css  the light palette, keyed on <html data-light-theme>
 *   theme-dark.css   the dark palette, keyed on <html data-dark-theme>
 *   components.css   what @primer/css v22 still ships: .btn, .Counter,
 *                    .branch-name, .markdown-body
 *
 * So there is no palette here, and no hex. The board used to carry a copy of
 * Primer's colours under its own names, which drifted the moment GitHub shipped
 * a token change and gave no way to notice. Now the values come from the package
 * and the only thing this file decides is WHICH token a thing should use.
 *
 * Four Primer components the board leans on no longer ship as CSS at all —
 * Primer's stylesheet was cut back when the design system moved to React, and
 * Label, StateLabel, Blankslate and Toast went with it. They are rebuilt here to
 * their published specs against the same tokens, grouped under "Primer
 * components rebuilt" and marked as such, so nobody mistakes them for house
 * style and nobody re-derives their measurements from a screenshot.
 */

/* The `hidden` attribute must actually hide.
   A UA stylesheet's `[hidden] { display: none }` has almost no specificity, so any
   class that sets display — .controls and .scroller are both flex — silently beats it
   and the element stays on screen with its attribute dutifully set. That is how the
   signed-out page kept showing its filters and Refresh button.
   Global and !important so the next element to need hiding does not rediscover it. */
[hidden] { display: none !important; }

* { box-sizing: border-box; }

html { height: 100%; }
body {
  margin: 0;
  padding: var(--space-lg);
  /* Exactly the viewport, and the page itself does not scroll. The board scrolls
     INSIDE it, the way Jira's does, so the columns keep their headings while the
     cards move under them — and the header, the filters and the account menu stay
     where you left them instead of sliding off the top on the way down a long
     column.
     min-height became height for the same reason it was min-height: the footer
     still needs pushing to the bottom on a short page, and now the bottom is a
     fixed place. */
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--bgColor-default);
  color: var(--fgColor-default);
  font: var(--text-body-shorthand-medium);
}

/* Octicons sit on the text baseline the way GitHub sets them, so an icon beside
   a word reads as part of the word rather than as a bullet before it. */
.octicon { display: inline-block; vertical-align: text-bottom; overflow: visible; flex: 0 0 auto; }

#head { margin-bottom: var(--space-sm); }
h1 { font: var(--text-title-shorthand-small); margin: 0; }

/* ================================================================
   Primer components rebuilt
   ----------------------------------------------------------------
   These four are Primer's, not the board's. @primer/css v22 no longer
   ships them, so the measurements below come from Primer's published
   specs and the colours from the vendored tokens. Treat them as
   vendored code: change them to track Primer, not to taste.
   ================================================================ */

/* Label — the outlined pill. Primer: 12px/18px, 0 7px, 1px border, pill radius,
   medium weight. Colour variants pair a fg role with its matching muted border,
   which is what keeps a row of them legible without a fill behind each one. */
.Label {
  display: inline-flex; align-items: center; gap: var(--space-xxs);
  padding: 0 7px;
  font-size: var(--text-body-size-small); line-height: 18px;
  font-weight: var(--base-text-weight-medium);
  white-space: nowrap;
  color: var(--fgColor-muted);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-full);
}
.Label--accent    { color: var(--fgColor-accent);    border-color: var(--borderColor-accent-muted); }
.Label--success   { color: var(--fgColor-success);   border-color: var(--borderColor-success-muted); }
.Label--attention { color: var(--fgColor-attention); border-color: var(--borderColor-attention-muted); }
.Label--danger    { color: var(--fgColor-danger);    border-color: var(--borderColor-danger-muted); }
.Label--secondary { color: var(--fgColor-muted);     border-color: var(--borderColor-muted); }
/* A Label that is a link. GitHub does this with IssueLabel — clicking a label
   filters by it — so an interactive label is the design system's own pattern,
   not an invention. The accent colour and the pointer are what say so. */
a.Label--link { text-decoration: none; cursor: pointer; }
a.Label--link:hover { color: var(--fgColor-accent); border-color: var(--borderColor-accent-muted); }
a.Label--link:hover .octicon { color: var(--fgColor-accent); }
a.Label--link:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: 1px; }

/* StateLabel — the filled pill GitHub puts at the top of a pull request. Always
   an icon plus a word: the colour is the fast read, the icon disambiguates it for
   anyone who cannot use the colour, and the word settles it. Primer's small size,
   because these sit on cards. */
.State {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  height: 24px; padding: 0 var(--space-sm);
  font-size: var(--text-body-size-small);
  font-weight: var(--base-text-weight-medium);
  line-height: 1; white-space: nowrap;
  color: var(--fgColor-onEmphasis);
  border: var(--borderWidth-thin) solid transparent;
  border-radius: var(--borderRadius-full);
  background-color: var(--bgColor-neutral-emphasis);
}
.State--open   { background-color: var(--bgColor-success-emphasis); }
.State--merged { background-color: var(--bgColor-done-emphasis); }
.State--closed { background-color: var(--bgColor-danger-emphasis); }
.State--draft  { background-color: var(--bgColor-neutral-emphasis); }
.State--queued { background-color: var(--bgColor-attention-emphasis); }

/* Counter — Primer's count pill. components.css only styles it inside a .btn, so
   the standalone case is defined here. */
.Counter {
  display: inline-block; min-width: 20px; padding: 0 6px;
  font-size: var(--text-body-size-small); line-height: 18px;
  font-weight: var(--base-text-weight-medium);
  text-align: center; font-variant-numeric: tabular-nums;
  color: var(--fgColor-default);
  background-color: var(--bgColor-neutral-muted);
  border: var(--borderWidth-thin) solid transparent;
  border-radius: var(--borderRadius-full);
}

/* VisuallyHidden — Primer's, verbatim. Text that is in the accessible name but
   not on screen. The :not(:focus) guard is Primer's own: a focusable element
   inside must be able to reveal itself when tabbed to. */
.VisuallyHidden:not(:focus):not(:active):not(:focus-within) {
  clip-path: inset(50%); height: 1px; width: 1px;
  overflow: hidden; position: absolute; white-space: nowrap;
}

/* SkeletonBox — Primer's loading placeholder, ported from @primer/react's
   experimental Skeleton (it has no CSS release, like the four above).
   Faithful to the source, including the parts that are not obvious:

     - The shimmer is a MASK, not a moving background. A gradient mask at 200%
       size slides its position from 200% to 0, which is why one rule works over
       any ground colour rather than needing a gradient per surface.
     - It does not animate under reduced motion AT ALL. Primer's prose guidance
       says loading animation should survive that preference, because it
       reassures you the system is not frozen; the component disagrees with the
       prose and gates the whole shimmer. The component is what GitHub ships, so
       it is what this follows.
     - forced-colors gets a transparent outline, which is the only way the box
       has a visible extent once the background colour is discarded. */
.SkeletonBox {
  display: block; height: 1rem;
  background-color: var(--skeletonLoader-bgColor);
  border-radius: var(--borderRadius-small);
}
@keyframes SkeletonBox-shimmer { from { mask-position: 200%; } to { mask-position: 0; } }
@media (prefers-reduced-motion: no-preference) {
  .SkeletonBox {
    animation: SkeletonBox-shimmer var(--base-duration-1000) infinite;
    mask-image: linear-gradient(75deg, #000 30%, rgb(0 0 0 / 0.65) 80%);
    mask-size: 200%;
  }
}
@media (forced-colors: active) {
  .SkeletonBox { outline: var(--borderWidth-thin) solid transparent; outline-offset: -1px; }
}

/* SkeletonText — also Primer's, and the formula matters. A text placeholder is
   the FONT SIZE tall with half the leading above and below it, so the box it
   occupies is a whole line. Getting this wrong is not cosmetic: a skeleton whose
   lines are font-size tall instead of line-height tall is short by the leading
   on every row, and the layout shifts by the accumulated difference the moment
   the real text arrives — which is the one thing a skeleton exists to prevent.

   Each variant only supplies the type pair; the height and the margin fall out
   of it, exactly as they do in SkeletonText. */
.SkeletonBox--text {
  --leading: calc(var(--font-size) * var(--line-height) - var(--font-size));
  height: var(--font-size);
  margin-block: calc(var(--leading) / 2);
}
.SkeletonBox--title {
  --font-size: var(--text-body-size-medium);
  --line-height: var(--text-body-lineHeight-medium);
}
.SkeletonBox--caption {
  --font-size: var(--text-body-size-small);
  --line-height: var(--text-body-lineHeight-small);
}
/* The metric figure is one line of title type. */
.SkeletonBox--figure {
  --font-size: var(--text-title-size-medium);
  --line-height: var(--text-title-lineHeight-medium);
  width: 3.5rem;
}
/* Primer's Counter is a fixed 18px pill, so this one is a box, not text. */
.SkeletonBox--counter { height: 18px; width: 1.5rem; border-radius: var(--borderRadius-full); }
/* Stands in for .metric-bar, which only one card has — but .metrics is a grid,
   so that one card sets the height of all four. Same box, same margins. */
.SkeletonBox--meter {
  height: 8px; margin: var(--space-xs) 0 var(--space-xxs);
  border-radius: var(--borderRadius-full);
}
/* The last line of a paragraph is short — the detail that makes a stack of bars
   read as text rather than as a table. Primer's own value. */
.SkeletonBox--last { max-width: 65%; min-width: 50px; }

/* Blankslate — what an empty container says for itself. Primer's compact variant:
   the board shows four of these at once on a quiet morning, and the full-size one
   would make an empty column taller than a full one. */
.blankslate {
  display: flex; flex-direction: column; align-items: center; gap: var(--space-xs);
  padding: var(--space-md) var(--space-sm);
  text-align: center;
  color: var(--fgColor-muted);
  font-size: var(--text-body-size-small);
}
.blankslate .octicon { color: var(--fgColor-muted); opacity: 0.6; }

/* Toast — Primer's floating confirmation. A coloured icon band on the leading
   edge and the message beside it, on the emphasis surface so it reads as an
   overlay rather than as another card that happens to be at the bottom. */
.toast {
  position: fixed; bottom: var(--space-lg); left: 50%; transform: translateX(-50%);
  z-index: var(--zIndex-overlay);
  display: flex; align-items: stretch;
  max-width: min(46rem, 92vw);
  font-size: var(--text-body-size-small);
  color: var(--fgColor-onEmphasis);
  background: var(--bgColor-emphasis);
  border-radius: var(--borderRadius-medium);
  box-shadow: var(--shadow-floating-medium);
  opacity: 0; pointer-events: none; transition: opacity 80ms cubic-bezier(0.33, 1, 0.68, 1);
}
.toast.show { opacity: 1; }
.toast-icon {
  display: flex; align-items: center; padding: var(--space-sm);
  background: var(--bgColor-accent-emphasis);
  border-radius: var(--borderRadius-medium) 0 0 var(--borderRadius-medium);
}
.toast.bad .toast-icon { background: var(--bgColor-danger-emphasis); }
.toast-body { padding: var(--space-sm) var(--space-md); align-self: center; }
.toast code {
  font-family: var(--fontStack-monospace); overflow-wrap: anywhere;
  /* On the emphasis surface the usual code chip would vanish, so it is a wash of
     the surface's own foreground rather than a bordered box. */
  background: rgb(255 255 255 / 0.15); border-radius: var(--borderRadius-small); padding: 0 var(--space-xxs);
}
@media (prefers-reduced-motion: reduce) { .toast { transition: none; } }

/* ================================================================
   The board's own components
   ================================================================ */

/* ---- controls ---- */

/* The filters live inside the scroll region because they sit between the strip
   and the board, but they are not lanes and must not travel with them: scrolled
   right to bring the last lane out from under the pane — the exact interaction
   the spacer exists to enable — they would slide off the left edge, taking the
   Refresh button and the status line with them.

   sticky + left:0 pins them to the scrollport while the lanes move underneath.
   align-self keeps the box only as wide as its contents, so the sticky area is
   the controls themselves rather than a stretched full-width band. */
/* In the header now, beside the account menu. The sticky/left positioning it
   carried was for life inside the horizontally scrolling board region, which it
   has left. */
.controls {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md);
  font-size: var(--text-body-size-small); color: var(--fgColor-muted);
}
.status { font-size: var(--text-caption-size); color: var(--fgColor-muted); }
.status.working { animation: fade 1.6s ease-in-out infinite; }
@keyframes fade { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .status.working { animation: none; } }

/* Primer's ToggleSwitch, small. A rounded rectangle rather than a pill — that is
   the shape Primer draws, and it is what tells a switch apart from a Label at a
   glance on a page that has plenty of both.

   Still a real checkbox underneath: appearance:none restyles the control without
   giving up the label association, the keyboard behaviour or the focus ring that
   a div-with-a-click-handler would have thrown away. */
.ToggleSwitch { display: inline-flex; align-items: center; gap: var(--space-xs); cursor: pointer; }
.ToggleSwitch input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto; width: 32px; height: 20px; margin: 0; position: relative; cursor: pointer;
  background: var(--controlTrack-bgColor-rest);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  transition: background-color 80ms cubic-bezier(0.5, 1, 0.89, 1), border-color 80ms cubic-bezier(0.5, 1, 0.89, 1);
}
.ToggleSwitch input[type="checkbox"]::after {
  content: ""; position: absolute; top: 1px; left: 1px;
  width: 16px; height: 16px; border-radius: var(--borderRadius-small);
  background: var(--controlKnob-bgColor-rest);
  box-shadow: var(--shadow-resting-small);
  transition: transform 80ms cubic-bezier(0.5, 1, 0.89, 1);
}
.ToggleSwitch input[type="checkbox"]:checked {
  background: var(--control-checked-bgColor-rest);
  border-color: var(--control-checked-borderColor-rest);
}
.ToggleSwitch input[type="checkbox"]:checked::after {
  background: var(--controlKnob-bgColor-checked);
  transform: translateX(12px);
}
.ToggleSwitch input[type="checkbox"]:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .ToggleSwitch input[type="checkbox"], .ToggleSwitch input[type="checkbox"]::after { transition: none; }
}

/* .btn comes from Primer, icon spacing and all. Only the focus ring is ours:
   @primer/css leaves it to the consumer, and a board with drag-and-drop needs one
   that is visible on every surface here. */
.btn:focus-visible, .btn-octicon:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: -2px; }
/* Primer's .btn-octicon is a bare glyph: no cursor, no border, no hover ground.
   It assumes a host page that has already dressed it, and ours had not — so the
   icon buttons showed an arrow, which is the one cue that says "not a control".

   The hover is GitHub's, and it is deliberately NEUTRAL: a grey ground and a
   hairline border, not an accent. Accent on hover reads as a link; these copy
   something, and the affordance for that is a button gaining an edge. */
.btn-octicon {
  cursor: pointer;
  margin-left: 0; padding: var(--space-xs);
  border: var(--borderWidth-thin) solid transparent;
  border-radius: var(--borderRadius-medium);
}
.btn-octicon:hover {
  color: var(--fgColor-default);
  background: var(--control-transparent-bgColor-hover);
  border-color: var(--borderColor-default);
}
.btn-octicon:active { background: var(--control-transparent-bgColor-active); }

/* Primer's tooltip is an ::after on the trigger, so an ancestor that scrolls
   clips it — and both places the board uses one, the pane and the card columns,
   are scroll containers. The direction variant is the fix rather than a z-index:
   -se opens down and to the right of the trigger instead of centring above it,
   which is what keeps a card-head tooltip inside the column it lives in.

   -no-delay because these are labels for icons that would otherwise be unreadable
   — Primer's default delay is right for a control whose text is already on
   screen, and wrong for one whose text is only in the tooltip. */
/* Primer assumes a host page that has already stripped anchor underlines. This
   page has not, so the sign-in button arrived wearing one. */
a.btn { text-decoration: none; }

/* ---- metrics, in Primer's Box shape ----

   Separate bordered cards rather than one shared strip, label above the figure
   rather than below it. The label is bright and the caption muted, so the reading
   order is what-this-is, how-big, why-it-is-honest. */

/* The metric strip and the board are ONE set of lanes, so they share their
   geometry: same count, same gap, same width per lane. They used to be two
   independent grids that happened to look aligned at some window sizes —
   auto-fit tracks up top, four fixed tracks below — which is why widening the
   region reflowed the strip instead of scrolling it.

   Flex with an explicit basis rather than grid, because the basis is the whole
   point: each lane is exactly one quarter of the region, so four lanes plus
   their gaps come to exactly the region's width and nothing overflows until the
   spacer says so. */
.metrics, .board { display: flex; gap: var(--lane-gap); align-items: stretch; }
/* Grow to fill whatever the metrics and the footer leave, and never shrink below
   the cards. Without the grow, a filtered board with two cards in it left the
   version number floating halfway up the screen with nothing under it. */
.board { flex: 1 0 auto; }
.metrics > .metric, .board > .col {
  flex: 0 0 calc((100% - (var(--lanes) - 1) * var(--lane-gap)) / var(--lanes));
  min-width: 0;
}
.metrics { margin-bottom: var(--space-xxs); }
.metric {
  display: flex; flex-direction: column; gap: var(--space-xxs);
  background: var(--bgColor-default);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  padding: var(--space-sm) var(--space-md);
}
.metric-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-sm); }
/* The label carries the definition. A number under a two-word heading is an
   invitation to assume what it counts, and every one of these has an edge that
   matters — when the clock starts, when it stops, what "outstanding" excludes. */
.metric-lbl.tip { cursor: pointer; }
.metric-lbl { font-size: var(--text-body-size-small); font-weight: var(--base-text-weight-semibold); color: var(--fgColor-default); }
.metric-figure { display: flex; align-items: baseline; gap: var(--space-xs); flex-wrap: wrap; }
.metric-num { font: var(--text-title-shorthand-medium); font-variant-numeric: tabular-nums; }
/* "46 of 78" — the denominator muted and small, because the figure is the point
   and the total is the context that stops it being meaningless. */
.metric-of { font-size: var(--text-body-size-small); color: var(--fgColor-muted); }
.metric-scope, .metric-sub { font-size: var(--text-caption-size); color: var(--fgColor-muted); }
.metric.breach .metric-num { color: var(--fgColor-danger); }

/* A bar earns its place on the one metric with a target: it shows the gap at a
   glance, which a percentage next to another percentage does not. */
.metric-bar {
  position: relative; height: 8px; margin: var(--space-xs) 0 var(--space-xxs);
  background: var(--bgColor-neutral-muted);
  border-radius: var(--borderRadius-full); overflow: hidden;
}
.metric-bar-fill {
  position: absolute; inset: 0 auto 0 0; background: var(--bgColor-success-emphasis);
  border-radius: var(--borderRadius-full);
}
.metric-bar-fill.over { background: var(--bgColor-danger-emphasis); }
/* The target as a tick on the track — where the bar SHOULD stop. */
.metric-bar-target {
  position: absolute; top: 0; bottom: 0; width: 2px;
  background: var(--fgColor-default); opacity: 0.55;
}

/* ---- team breakdown: collapsed, because it answers the follow-up question ---- */

.breakdown { font-size: var(--text-body-size-small); }
.breakdown .as-of { margin-bottom: var(--space-xs); }
.breakdown-scroll { overflow-x: auto; }
.breakdown table { border-collapse: collapse; width: 100%; font-size: var(--text-caption-size); }
.breakdown th, .breakdown td {
  text-align: left; white-space: nowrap;
  padding: var(--space-xxs) var(--space-sm) var(--space-xxs) 0;
  border-bottom: var(--borderWidth-thin) solid var(--borderColor-muted);
}
.breakdown th { color: var(--fgColor-muted); font-weight: var(--base-text-weight-semibold); }
.breakdown td.num, .breakdown th.num { text-align: right; font-variant-numeric: tabular-nums; }
.breakdown td.over { color: var(--fgColor-danger); font-weight: var(--base-text-weight-semibold); }
.breakdown tr.mine-row td { background: var(--bgColor-accent-muted); }
.breakdown tbody tr:last-child td { border-bottom: none; }
.breakdown .as-of strong.over { color: var(--fgColor-danger); }

/* ---- columns ----

   Depth follows GitHub's: the page is the canvas, columns recede into the inset,
   cards come forward on the default surface with a border. */

/* Lane count is a single variable so the strip and the board can never disagree
   about it at a breakpoint. */
:root { --lanes: 4; --lane-gap: var(--space-md); }
@media (max-width: 70rem) { :root { --lanes: 2; } }
@media (max-width: 38rem) { :root { --lanes: 1; } }

.col {
  background: var(--bgColor-muted);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  /* The heading above draws the top edge — see .heads. Square at the top so the
     two boxes meet as one, rounded at the bottom where the lane actually ends. */
  border-top: none;
  border-radius: 0 0 var(--borderRadius-medium) var(--borderRadius-medium);
  padding: var(--space-sm); display: flex; flex-direction: column; min-height: 5rem;
}

/* ---- your work, then everyone else's, in the same columns ----------------
   Two bands under one set of headings. Splitting the board into two boards
   would have been simpler and wrong: the column is a STAGE, and a PR of yours
   in review is at the same stage as anybody else's, so duplicating the headings
   would say there were eight stages when there are four.

   The bands have to line up ACROSS columns — a divider that sat at a different
   height in each column would read as four unrelated rules rather than one
   split. That is what subgrid is for: .board owns the rows, and each column
   inherits them, so every band takes the height of the tallest cell in its row
   and the rule lands level. A column with one card of yours where another has
   three simply carries the space.

   Only applied when there is something to split — see render(). */
.board.is-split {
  display: grid;
  /* The SAME fixed width the flex rows give their children, not 1fr.
     1fr shrinks to fit its container, and the metrics and headings do not — so
     when the pane's spacer appeared as a fifth item, the flex rows kept their
     width and overflowed while the grid quietly narrowed all four columns. The
     board slid out of alignment with the headings above it the moment a card was
     opened. Fixed here means all three rows overflow together and scroll
     together. */
  grid-template-columns: repeat(var(--lanes), calc((100% - (var(--lanes) - 1) * var(--lane-gap)) / var(--lanes)));
  /* The spacer's column, placed explicitly below. */
  grid-auto-columns: var(--detail-width);
  /* heading, yours, the rule, everyone else's */
  grid-template-rows: auto auto auto auto;
  align-items: stretch;
}
.board.is-split > .col {
  grid-row: 1 / -1;
  display: grid;
  grid-template-rows: subgrid;
  row-gap: 0;
}
.band { display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; }
/* Empty is a state, not a gap: a band with nothing in it still holds its row
   open so the rule stays level, and 0 would collapse it. */
.band:empty { min-height: var(--space-sm); }
/* The nudge control, at the head of your own To-review band.
   Sits above the swimlanes rather than in the column heading, because the
   heading covers both bands and this is about your half. Full width so it reads
   as a header for what follows, not as a card that lost its content. */
.band-actions { display: flex; justify-content: flex-start; margin-bottom: var(--space-xs); }
.band-actions .btn { width: 100%; justify-content: center; gap: var(--space-xs); }

.band-rule {
  border-top: var(--borderWidth-thin) dashed var(--borderColor-muted);
  margin: var(--space-sm) 0;
}
/* The pane's scroll spacer is a flex item, and this board is not flex. Placing
   it explicitly beyond the last lane keeps it out of the band rows, where auto
   placement would have started a fifth row. */
/* .board grows to fill; .lanes is the flex parent now, so the rule moves up. */
.lanes > .board { flex: 1 0 auto; }
/* ---- the column headings ------------------------------------------------
   A row of their own, sticky at the top of the scrolling region. Nothing can
   scroll behind them, because the columns start below them: the notches at
   their rounded top corners look out onto the page, which is what is above a
   column anyway.

   .lanes holds the headings and the board with no gap between them, so the two
   boxes meet — .scroller's own gap would have left a stripe of page between a
   heading and the column it names. */
.lanes { display: flex; flex-direction: column; flex: 1 0 auto; min-width: 0; }
.heads {
  display: flex; gap: var(--lane-gap); align-items: stretch;
  position: sticky; top: 0; z-index: 3;
  /* Opaque across the WHOLE strip, gaps included. Sticky alone is not enough:
     the metrics scroll up behind this row, and the notches outside each
     heading's rounded top corners are transparent — so a metric card's borders
     showed through them as a pair of ticks above every column. That was the
     spikes, and it was never the columns doing it.

     The page colour, because the page is what should be above a column. */
  background: var(--bgColor-default);
}
.heads > .head {
  flex: 0 0 calc((100% - (var(--lanes) - 1) * var(--lane-gap)) / var(--lanes));
  min-width: 0;
  font-size: var(--text-body-size-medium); color: var(--fgColor-default);
  font-weight: var(--base-text-weight-semibold);
  margin: 0; padding: var(--space-sm);
  display: flex; align-items: center; gap: var(--space-xs);
  background: var(--bgColor-muted);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-bottom: none;
  border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0;
}
/* The count group sits at the far edge of the header, whatever it holds. */
.col-count { margin-left: auto; display: inline-flex; align-items: baseline; gap: var(--space-xs); }
.of { color: var(--fgColor-muted); font-weight: var(--base-text-weight-normal); font-size: var(--text-caption-size); }
.col.drop-target { outline: 2px dashed var(--focus-outlineColor); outline-offset: -2px; }

/* ---- the first load ----

   Primer's loading guidance is duration-driven: show nothing under a second,
   and use a skeleton for a large content area so the shape of the page arrives
   before its contents. The open-PR query takes about four seconds (see the
   note on the singleflight in server.go), and until now the grid was simply
   blank for all of it.

   Only the FIRST load. A refresh already has a board on screen, and replacing
   real cards with grey bars to fetch a newer copy of the same thing is the
   jarring shift the guidance warns about — the header's status line carries a
   refresh instead.

   The 300ms delay is Primer's `delay="short"`, and it is what keeps this honest
   for a fast response: the skeleton is painted transparent and fades in only if
   the wait outlasts the threshold, so a cached board never flashes one. Done in
   CSS rather than with a timer, so there is no second render to schedule and
   nothing to cancel when the data beats it. */
/* The fade goes on the lanes themselves, with no wrapper element.
   A wrapper was the obvious way to hold the skeleton, and `display: contents`
   looked like it made the wrapper disappear — but only for LAYOUT. Selectors
   still see the real tree, so `.board > .col` stopped matching once the columns
   were grandchildren, the lanes lost their flex basis and the whole skeleton
   collapsed to content width. The child combinator is load-bearing here. */
.is-skeleton { opacity: 0; animation: skeleton-appear 120ms ease-out 300ms forwards; }
@keyframes skeleton-appear { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .is-skeleton { animation-duration: 0s; } }
/* A disabled filter still has to look like the control it is, just inert. */
/* The refresh button, when there is something to refresh. Same glyph, same word,
   same place — only the colour moves, because what has changed is whether
   pressing it would do anything.

   A filled chip rather than an outline: the border takes the background colour,
   so the shape reads as one warm block rather than a box drawn around a button.
   Only ever a change of appearance — the board itself never moves until this is
   pressed, because one that rearranges under the cursor is worse than one a
   minute out of date. */
#refresh.has-updates {
  background: var(--bgColor-attention-muted);
  border-color: var(--bgColor-attention-muted);
  color: var(--fgColor-attention);
  font-weight: var(--base-text-weight-semibold);
}
#refresh.has-updates .octicon { color: var(--fgColor-attention); }
/* Hover underlines and nothing else. Primer's .btn:hover repaints the ground and
   the border, which on a chip that is already coloured to mean something reads as
   the meaning changing rather than the pointer arriving. The colours are repeated
   here to hold them still. */
#refresh.has-updates:hover,
#refresh.has-updates:focus-visible {
  background: var(--bgColor-attention-muted);
  border-color: var(--bgColor-attention-muted);
  color: var(--fgColor-attention);
  text-decoration: underline;
}
/* Primer's Spinner turns once a second, linear — an indeterminate indicator has
   no progress to convey, so any easing would imply one.

   Laid out exactly as .octicon is, because it REPLACES one: an inline SVG sits on
   the baseline by default, where an octicon is pulled down to text-bottom and
   carries a right margin. Without both, swapping the icon for the spinner lifted
   it above the label and closed the gap — the button visibly jolting at the
   moment of being pressed, which is the one moment it should look steady. */
.Spinner {
  display: inline-block; vertical-align: text-bottom; margin-right: var(--base-size-4);
  animation: spinner-rotate 1s linear infinite; transform-origin: center;
}
@keyframes spinner-rotate { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  /* Fade rather than turn. The state still has to be legible — a press with no
     acknowledgement at all reads as a dead button. */
  .Spinner { animation: spinner-fade 1.4s ease-in-out infinite; }
  @keyframes spinner-fade { 50% { opacity: 0.35; } }
}
/* aria-disabled, not disabled — so Primer's disabled styling never applies and
   the button must not dim itself either. It is working, not unavailable. */
#refresh[aria-disabled="true"] { cursor: default; opacity: 1; }
#refresh[aria-disabled="true"]:hover { text-decoration: none; }

.controls input:disabled, .controls button:disabled { cursor: default; opacity: 0.6; }
.ToggleSwitch:has(input:disabled) { cursor: default; color: var(--fgColor-muted); }
/* The story head holds two bars while loading, so it lays them out like the
   ticket and count it stands in for. */
.story-head .SkeletonBox { display: inline-block; }
.skeleton-card {
  background: var(--bgColor-default);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-left: var(--borderWidth-thicker) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);
  display: flex; flex-direction: column; gap: var(--space-xs);
}
/* The head row is a short number and a short age at opposite ends, which is the
   silhouette a real card has. */
.skeleton-head { display: flex; justify-content: space-between; gap: var(--space-sm); }
.skeleton-head .SkeletonBox { width: 3.5rem; }
.skeleton-head .SkeletonBox:last-child { width: 1.75rem; }
.skeleton-meta { display: flex; gap: var(--space-xs); }
.skeleton-meta .SkeletonBox { width: 4.5rem; border-radius: var(--borderRadius-full); }
.skeleton-meta .SkeletonBox:last-child { width: 3rem; }

/* ---- stories ---- */

.story { border-left: var(--borderWidth-thick) solid var(--borderColor-muted); padding-left: var(--space-sm); margin-bottom: var(--space-md); }
.story-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-xs); font-size: var(--text-body-size-small); font-weight: var(--base-text-weight-semibold); margin-bottom: var(--space-xs); }
.story-count { font-size: var(--text-caption-size); color: var(--fgColor-muted); font-weight: var(--base-text-weight-normal); }

/* ---- cards ---- */

.card {
  background: var(--bgColor-default);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  /* The left edge is a status stripe, and its resting state is a COLOUR rather
     than none. Transparent let the column's own background through, which on a
     card that means nothing to you read as a border that had failed to draw —
     black against a dark theme, white against a light one, and in both cases a
     hole rather than a state.

     The ladder is: this, for a PR you have no part in; emphasis, for one you are
     watching that is not your move; green, for one that is. Quietest to loudest,
     and every rung visible. */
  border-left: var(--borderWidth-thicker) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  padding: var(--space-xs) var(--space-sm); margin-bottom: var(--space-xs);
  display: flex; flex-direction: column; gap: var(--space-xxs);
}
.card[draggable] { cursor: grab; }
.card:hover { border-color: var(--borderColor-emphasis); }
.card.dragging { opacity: 0.4; }
.card.is-blocked { opacity: 0.62; }
.card.is-selected { outline: 2px solid var(--focus-outlineColor); outline-offset: -2px; }

.card-head { display: flex; align-items: center; gap: var(--space-xs); font-size: var(--text-body-size-small); }
.card-head .num {
  display: inline-flex; align-items: center; gap: var(--space-xxs);
  color: var(--fgColor-muted); text-decoration: none; font-variant-numeric: tabular-nums;
}
.card-head .num:hover { color: var(--fgColor-accent); text-decoration: underline; }
/* The PR-state octicon carries the state on the number itself, the way GitHub
   puts it beside a PR everywhere it lists one. */
.card-head .num .octicon { color: var(--fgColor-muted); }
.card-head .num.state-open .octicon   { color: var(--fgColor-success); }
.card-head .num.state-merged .octicon { color: var(--fgColor-done); }
.card-head .num.state-closed .octicon { color: var(--fgColor-danger); }
.card-head .num.state-draft .octicon  { color: var(--fgColor-muted); }
.card-head .num.state-queued .octicon { color: var(--fgColor-attention); }
.card-head .stack { display: inline-flex; align-items: center; gap: var(--space-xxs); color: var(--fgColor-muted); }
/* Whether this card is yours, beside the number rather than spelled out in the
   body. Accent because it is the one mark on the card that is about you.

   cursor: help, because the card is draggable and would otherwise offer a grab
   hand over a thing that cannot be grabbed on its own — and because the icon has
   an explanation behind it, which is exactly what that cursor promises. */
.card-head .mine { display: inline-flex; align-items: center; color: var(--fgColor-accent); }
/* Stale is a statement about the age, so it sits beside it. Attention rather than
   danger: nobody has picked this up, which is a prompt, not a fault. */
.card-head .stale { display: inline-flex; align-items: center; flex: none; }
.card-head .stale .octicon { color: var(--fgColor-danger); }

/* Anything that explains itself on hover gets the pointer, the way GitHub's
   hovercard triggers do — and NOT the grab hand the card would otherwise offer,
   since none of these is a drag handle even though the card is draggable. */
.tip, .card-head .stack, .card-head .age { cursor: pointer; }
/* Author then age, both anchored right. The auto margin moves here from .age so
   the pair travels together and the age stays last — the byline is a fact about
   the PR, the age is a fact about the clock, and reading them in that order is
   what a GitHub list does. */
.card-head .byline {
  margin-left: auto; color: var(--fgColor-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The age escalates as it approaches the target, so a card can be caught before
   it breaches rather than after. Danger for past matches the metric, whose own
   figure turns danger when the board is over — same event, two places. */
.card-head .age { color: var(--fgColor-muted); flex: none; }
.card-head .age.near { color: var(--fgColor-attention); }
.card-head .age.past { color: var(--fgColor-danger); font-weight: var(--base-text-weight-semibold); }
.card-title { font-weight: var(--base-text-weight-medium); overflow-wrap: anywhere; }
/* Right-aligned, to sit under the byline and age rather than under the title.
   These are activity counts — how far through your comments, how much has moved —
   and reading them as a column down the card's right edge is what makes a board
   of them scannable. */
.card-meta {
  font-size: var(--text-caption-size); color: var(--fgColor-muted);
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  gap: var(--space-xs); align-items: center;
}
.claim { display: inline-flex; align-items: center; gap: var(--space-xxs); color: var(--fgColor-muted); }

/* The assignee: a face, in the same place on every card. */
.assignee { display: inline-flex; align-items: center; gap: var(--space-xxs); }
.assignee .avatar { width: 18px; height: 18px; background: var(--bgColor-neutral-muted); }
/* The in-flight spinner stands exactly where the face will be, so the row does
   not shift when the answer arrives. */
.assignee .Spinner { width: 18px; height: 18px; margin: 0; color: var(--fgColor-muted); }
/* Nobody holding it is a state worth drawing, not an absence — so it occupies the
   same 18px circle an avatar would, in the same place, with a generic person in
   place of a face. Not a pill: a pill is a different KIND of thing from a face,
   and the slot has to read as the same slot whether or not it is filled.

   Dashed, which is the one difference that matters: it says empty seat rather
   than person whose picture failed to load. */
.assignee .empty-face {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; box-sizing: border-box;
  border: var(--borderWidth-thin) dashed var(--borderColor-emphasis);
  border-radius: var(--borderRadius-full); color: var(--fgColor-muted);
}

/* The bottom of the card: the specialists purr chose, who reviewed it and what
   they said, then the PR's own condition. Anchored, because these used to sit in .card-meta, which wraps — the
   same pill landed on a different line depending on how long the author's name
   was, and you had to hunt for it card by card. A fixed place is worth more here
   than compactness: the eye learns one. */
.card-foot {
  display: flex; flex-direction: column; gap: var(--space-xxs);
  margin-top: var(--space-xs); padding-top: var(--space-xs);
  border-top: var(--borderWidth-thin) solid var(--borderColor-muted);
}
.card-foot .condition { display: flex; justify-content: flex-end; }

/* The specialists purr chose, the foot's first row, shaped like a reviewer row
   below it: one line, caption size, names left and the count where a verdict
   would sit. Names a weight up and separators a shade down, no boxes. Clipped
   rather than wrapped, so the row is the same height on every card. */
.card-specialists {
  display: flex; align-items: center; gap: var(--space-xs);
  font-size: var(--text-caption-size); line-height: 18px;
  color: var(--fgColor-muted); min-width: 0;
}
/* Inline text in a block, not flex items, because only a line of text can end
   in an ellipsis. */
.card-specialists-list {
  display: block; flex: 1 1 auto; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.card-specialists-list > span:not(.card-specialists-sep) { font-weight: var(--base-text-weight-medium); }
.card-specialists-sep { color: var(--borderColor-default); margin: 0 3px; }
.card-specialists-more { margin-left: auto; flex: none; cursor: pointer; }

/* One reviewer, GitHub's way round: face and name left, verdict right. */
.reviewer {
  display: flex; align-items: center; gap: var(--space-xs);
  font-size: var(--text-caption-size); color: var(--fgColor-muted);
  min-width: 0;
}
.reviewer .avatar { width: 16px; height: 16px; flex: none; background: var(--bgColor-neutral-muted); }
/* NOT .who: that class is the header's account menu, which is margin-left:auto —
   inherited here it pushed the reviewer's name into the middle of the row, with
   the verdict's own auto margin holding the other side. Same name, two places,
   and the more specific selector did not fix it because it set no margin to
   override. */
.reviewer .rev-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The auto margin is what draws the eye along the row to the verdict, and what
   makes a column of reviewers scannable down the right-hand edge. */
.reviewer .verdict { margin-left: auto; display: inline-flex; flex: none; }
.reviewer .verdict-ok .octicon { color: var(--fgColor-success); }
.reviewer .verdict-changes .octicon { color: var(--fgColor-attention); }
/* Muted, and deliberately not a verdict colour: someone who left notes without
   approving has not blessed anything, and colouring it would say they had. */
.reviewer .verdict-said .octicon { color: var(--fgColor-muted); }

/* Asked of you: an accent, not a filter. Other teams' reviews stay fully visible. */
.card.is-mine { border-left-color: var(--fgColor-accent); background: var(--bgColor-accent-muted); }

/* Watched reviews, coloured by WHOSE MOVE IT IS — the question the verdict on the
   card's tooltip answers in words. Warm is waiting on you, cool is waiting on
   them, and the two are deliberately far apart: "you claimed it" (never reviewed)
   and "you reviewed it" (done for now) were rendering identically, which is the
   one distinction a reviewer needs at a glance.

   Primer's muted backgrounds do this job directly, so the tints are tokens now
   rather than a colour-mix over the card surface: --bgColor-success-muted IS the
   wash GitHub puts behind a positive row.

   Written after .card.is-mine and scoped through .col so they beat it on
   SPECIFICITY, not on source order: is-mine sets border-left and background at the
   same specificity, and since a claimed review is nearly always one that was asked
   of you, it otherwise hides every accent — green and amber included. */
/* TWO answers, not five states. The states differ in why a card is yours —
   never reviewed, they replied, they pushed — and that distinction belongs in a
   sentence, which the pane has room for. The card has a colour, and a colour can
   only carry the question actually being asked: is this mine?

   Painting the reason instead was read exactly backwards. Amber for "they pushed,
   go and re-read" landed as "pending, waiting on them", because that is what
   amber means everywhere else — while grey for "genuinely with the author" read
   as no signal at all. Green already read correctly as "yours", so green is the
   one kept. The file-diff count in the meta row now carries what amber used to.

   Green, and nothing. A second "not yours" colour would only re-pose the question
   the first one just answered. */
.col .card.watched-unreviewed,
.col .card.watched-responded,
.col .card.watched-changed,
.col .card.watched-answer,
.col .card.watched-merge {
  border-left-color: var(--fgColor-success);
  background: var(--bgColor-success-muted);
}
.col .card.watched-rebased,
.col .card.watched-waiting,
.col .card.watched-merging {
  border-left-color: var(--borderColor-emphasis);
  background: var(--bgColor-default);
}

.error { color: var(--fgColor-danger); }

/* ---- layout: the board, with the pane laid over it ----

   The pane used to take a grid column, which squashed the board from four
   columns into a narrower four and broke the alignment between the columns and
   the metric cards above them — the two rows stopped being the same grid.

   So it overlays instead, the way Jira's work-item panel does. Two things follow
   from that, and the second is the one that is easy to get wrong:

   1. The metrics and the board share ONE scroll region, because they are the
      same four columns and have to move together. Scroll them separately and the
      alignment that motivated all of this survives only until you scroll.

   2. That region scrolls, NOT the page. Growing the document would drag the
      brand row and the account menu sideways along with everything else, and
      leave a scrollbar under the whole window rather than under the board.

   min-width is what creates the scroll inside the region. Padding would not:
   padding shrinks the content box, so the grid tracks would narrow again and we
   would be back to squashing by another route. */
.scroller {
  display: flex; flex-direction: column; gap: var(--space-sm);
  /* The one scrolling region. flex 1 with min-height 0 is what lets it take the
     space the header leaves and no more — without the min-height a flex item
     refuses to shrink below its content, so the region grows and the page
     scrolls again, which is the thing being fixed. */
  flex: 1 1 auto; min-height: 0;
  overflow: auto;
  /* Reserve the scrollbar's width instead of letting it overlay. An overlay
     scrollbar drawn over the last column's right border reads as the border
     failing to draw, which is a bug people report rather than a scrollbar they
     ignore. */
  scrollbar-gutter: stable;
  /* position:relative is load-bearing, not decoration. Without it the region
     scrolls correctly AND the document still grows: the grid's overflow leaks
     into the root scrolling box, so the page gained its own horizontal scrollbar
     on top of this one. Making the region a containing block keeps the overflow
     where it belongs.

     `contain: paint` fixes it too. This is not chosen over it for any behavioural
     reason — overflow-x:auto already makes this a clipping box on BOTH axes (a
     computed `visible` becomes `auto` when its partner is not `visible`), so the
     team-breakdown popover anchored in the metric strip is confined either way.
     It fits inside the region today, which is why it looks unclipped. Prefer the
     containing block because it is the narrower change, and note the constraint:
     anything anchored in here that must escape needs the top layer, not a
     z-index. */
  position: relative;
  /* Room for the region's own scrollbar, so it never sits on top of a card. */
  padding-bottom: var(--space-xxs);
}
:root { --detail-width: clamp(24rem, 42vw, 46rem); }
@media (max-width: 60rem) { :root { --detail-width: 100vw; } }

/* How the pane makes room for itself, without touching the layout.

   An empty lane exactly the pane's width, at the end of the strip and of the
   board. It costs nothing when hidden and, when shown, makes the content one
   pane wider than the region — so the region scrolls, by exactly enough to bring
   the last real lane out from under the pane. The lanes themselves never change
   width, which is the failure every previous attempt shared: min-width stretched
   the strip's auto-fit tracks, and padding shrank the content box.

   :has() rather than a class set from JavaScript. The pane being open is already
   expressed in the DOM; asking CSS to read it keeps layout entirely in the
   stylesheet, and removes a class that could be left behind on an error path. */
/* An empty lane at the end of each row, so the board can scroll OUT from under
   the pane rather than being squeezed into what is left of the window. Padding
   on the scroll region was tried and is worse: it shrinks the content box, so
   all four columns narrow and the board becomes unreadable while a card is open.
   Rendered always; shown only while the pane is open. aria-hidden because it is
   pure geometry — there is nothing in it to announce. */
.lane-spacer { display: none; flex: 0 0 var(--detail-width); }
body:has(.detail.is-open) .lane-spacer { display: block; }
.board.is-split > .lane-spacer { grid-column: calc(var(--lanes) + 1); grid-row: 1 / -1; }

/* ---- the empty reviews state ----
   One line and the button beside it, with the caveat underneath. Compact because
   it is the common case and the specialists breakdown sits below it. */
.cta {
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  background: var(--bgColor-muted);
  padding: var(--space-xs) var(--space-sm);
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-xxs) var(--space-sm); align-items: center;
}
.cta-lead {
  margin: 0; font-weight: var(--base-text-weight-semibold);
  color: var(--fgColor-default); font-size: var(--text-body-size-medium);
}
.cta-actions { grid-column: 2; grid-row: 1 / span 2; display: flex; gap: var(--space-xs); }
.cta-note {
  margin: 0; color: var(--fgColor-muted);
  font-size: var(--text-body-size-small); line-height: 1.5;
}

/* ---- why purr chose the specialists ----
   Two columns of name-and-score rows, filled down then across, so the chosen
   ones read as a block before the ones left out. The score column is fixed
   width so scores line up down each column. */
.extra:has(.triage-all) { margin-top: var(--space-md); }
.triage-list {
  list-style: none; margin: 0; padding: 0;
  columns: 2; column-gap: var(--space-lg);
}
.triage-row {
  display: grid; grid-template-columns: 1rem minmax(0, 1fr) 2.5rem;
  gap: var(--space-xs); align-items: baseline; padding: 1px 0;
  font-size: var(--text-body-size-small);
  break-inside: avoid;
}
.triage-mark { color: var(--fgColor-success); }
.triage-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.triage-why { color: var(--fgColor-muted); font-variant-numeric: tabular-nums; text-align: right; }
.triage-why.is-doubt { color: var(--fgColor-attention); }
.triage-row.is-out, .triage-row.is-out .triage-mark { color: var(--fgColor-muted); }
.triage-all summary {
  cursor: pointer; font-size: var(--text-caption-size); color: var(--fgColor-muted);
  margin-bottom: var(--space-xxs);
}

/* ---- the detail pane ---- */

.detail:not(.is-open) { display: none; }
/* Fixed to the viewport, full height, so it stays put while the board scrolls
   under it — the same shape as Jira's work-item panel. Full height rather than
   starting below the header: a panel that stops short of the top reads as a
   floating box that happens to be tall, not as a second surface. */
.detail {
  position: fixed; inset: 0 0 0 auto;
  width: var(--detail-width); z-index: 25;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--space-md);
  background: var(--bgColor-default);
  border-left: var(--borderWidth-thin) solid var(--borderColor-default);
  box-shadow: var(--shadow-floating-large);
}
/* ---- modal mode ----
   The same pane, taking the whole viewport, with the description beside the
   reviews instead of above them. Jira's shape, and for Jira's reason: a side
   panel is right for a glance and wrong for reading, and the thing you most want
   while reading a review is the change it is about, on screen at the same time.

   The pane keeps its identity — same element, same markup, same close button —
   so switching modes is a class and a body layout, not a second implementation
   of the detail pane that would drift from the first. */
.detail.is-modal {
  inset: var(--space-md);
  width: auto;
  max-width: 92rem;
  margin-inline: auto;
  /* Generous side padding. The slide-in panel is narrow and every pixel of it is
     content; a modal is chosen for room, and text running to the frame is the
     thing that makes a full-screen view feel worse than the panel it replaced. */
  padding: var(--space-md) var(--space-xl) var(--space-xl);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-large);
}
/* The head's negative margins have to match the padding they are cancelling, or
   the sticky header sits inset from the edges it is supposed to span. */
.detail.is-modal .detail-head {
  margin: calc(var(--space-md) * -1) calc(var(--space-xl) * -1) var(--space-md);
  padding: var(--space-md) var(--space-xl) var(--space-sm);
}
/* The board behind it is not interactive while a modal covers it, and it should
   not look as though it is. Dimmed rather than hidden: the board staying faintly
   visible is what says this is a layer over your work rather than a page you
   navigated to. Sits just under the pane's z-index, and closes on click. */
.scrim[hidden] { display: none; }
.scrim {
  position: fixed; inset: 0; z-index: 24;
  background: var(--overlay-backdrop-bgColor, rgb(0 0 0 / 0.5));
}
/* The lane spacer exists to keep the board's right-hand column clear of a slide-in
   panel. A modal covers the board entirely, so reserving width for it would leave
   an unexplained gap down the side of a board nobody can see anyway. */
body:has(.detail.is-modal) .lane-spacer { display: none; }

.detail-cols {
  display: grid;
  /* A wide gutter, not the board's usual gap. Two columns of prose separated by
     a card-sized gap read as one column that has been broken in half; the gutter
     is what tells the eye they are two things. */
  gap: var(--space-xl);
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
}
/* min-width:0 on the items too: a grid item will not shrink below its content,
   and both columns hold things that refuse to wrap — code spans in a
   description, file paths in a finding. Without it the columns push each other
   out of the pane instead of scrolling inside it. */
.detail-col { min-width: 0; }
/* One column under about a laptop's width. Two 20rem columns are worse than one
   40rem column for both jobs, and a modal on a narrow screen is being used to
   get MORE room, not to prove it has two of something. */
@media (max-width: 60rem) {
  .detail-cols { grid-template-columns: minmax(0, 1fr); }
  .detail.is-modal { inset: 0; border-radius: 0; border: none; }
}

/* The identity of the PR rides along at the top of the panel rather than
   scrolling out of reach: number, state, title, author, branch, actions, and the
   close button that — with the panel over the account menu — is the way back.
   Scroll into a long description and the head still says what you are reading
   and still offers checkout and sign off; only the close button used to. */
.detail-head {
  position: sticky; top: calc(var(--space-md) * -1); z-index: 1;
  margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) var(--space-sm);
  padding: var(--space-md) var(--space-md) var(--space-sm);
  background: var(--bgColor-default);
  /* The head now covers content rather than sitting above blank space, so it
     needs an edge — without one the body's first line looks half-scrolled under
     nothing. */
  border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);
  display: flex; flex-direction: column; gap: var(--space-sm);
}
/* A sticky box taller than its scrollport pins at the top and never moves, which
   would put the body permanently out of reach. On a window too short to carry
   the head AND something to read, the head goes back to scrolling with the rest
   — losing the pinned close button is the lesser failure by a distance. */
@media (max-height: 38rem) { .detail-head { position: static; } }
/* Number, then state, then the close button pushed to the far edge. The state
   belongs next to the thing it is the state OF; centred between two unrelated
   controls it read as a third, equal item. */
.detail-head-top { display: flex; align-items: center; gap: var(--space-sm); }
/* The controls travel as a group, pushed to the trailing edge.
   The auto margin is on the WRAPPER, not on each button. It used to be on every
   .btn-octicon in the row, which was correct while there was exactly one: with
   two, flexbox distributes the free space between both auto margins and the
   first button lands in the middle of the row looking like a third, unrelated
   thing. A wrapper also means adding a control later cannot resurrect that.
   Scoped to the top row regardless: the branch row's copy button is a
   .btn-octicon inside the head too, and it belongs beside the name it copies. */
.detail-head-controls { margin-left: auto; display: flex; align-items: center; gap: var(--space-xs); }
.detail-head .num {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  color: var(--fgColor-muted); text-decoration: none; font-variant-numeric: tabular-nums;
}
.detail-head .num:hover { color: var(--fgColor-accent); text-decoration: underline; }
.detail-body { display: flex; flex-direction: column; gap: var(--space-sm); }

/* The byline, as GitHub writes one: a face, a login, and when it opened. */
.detail-byline {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-xs);
  font-size: var(--text-body-size-small); color: var(--fgColor-muted);
}
.detail-author {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  color: var(--fgColor-default); text-decoration: none;
}
.detail-author:hover { color: var(--fgColor-accent); text-decoration: underline; }
.detail-byline .avatar { width: 16px; height: 16px; }

/* The remaining facts, each with the octicon that names it. */
.detail-facts { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-sm); align-items: center; }
.detail-fact {
  display: inline-flex; align-items: center; gap: var(--space-xxs);
  font-size: var(--text-body-size-small); color: var(--fgColor-muted);
}
.detail-fact .octicon { color: var(--fgColor-muted); }
.detail h3 { font: var(--text-title-shorthand-small); margin: 0; overflow-wrap: anywhere; }
.detail h4 {
  font-size: var(--text-caption-size); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--fgColor-muted); margin: 0 0 var(--space-xs); font-weight: var(--base-text-weight-semibold);
}
.detail-section { border-top: var(--borderWidth-thin) solid var(--borderColor-muted); padding-top: var(--space-sm); }
/* Unless it is the first thing under the head, whose border already draws that
   line — two rules a few pixels apart read as a mistake. When a watch line or a
   blocked warning comes first, the section keeps its own rule and this does not
   fire. */
.detail-body > .detail-section:first-child { border-top: none; padding-top: 0; }
/* The hand-off buttons, at Primer's default size and pushed to the trailing edge.
   Full size rather than .btn-sm: these are the pane's primary controls and GitHub
   sets the equivalents ("Close pull request", "Comment") at the default size — at
   btn-sm they read as a secondary toolbar under the thing that matters.
   Right-aligned for the same reason GitHub right-aligns its own: it puts them
   where the eye lands after reading the head, and keeps a ragged wrapped row
   anchored to one edge instead of two. */
.detail-actions {
  display: flex; flex-wrap: wrap; gap: var(--space-xs);
  align-items: center; justify-content: flex-end;
}
/* A direct child of the pane's flex column, so without this it stretches to the
   full width and reads as a banner rather than as the button it is. */
.detail-body > .btn { align-self: flex-start; }
.detail .warn { color: var(--fgColor-attention); font-size: var(--text-body-size-small); margin: 0; }
.as-of { font-size: var(--text-caption-size); color: var(--fgColor-muted); margin: 0; }

/* Diffstat. Primer dropped the .diffstat class along with Label and StateLabel,
   but the roles it was built from are unchanged: additions read as success,
   deletions as danger. Tabular figures so +1758 and −14 line up rather than
   wobbling as the numbers change. */
.diffstat {
  display: inline-flex; align-items: baseline; gap: var(--space-xs);
  font-size: var(--text-caption-size); font-variant-numeric: tabular-nums;
}
.diffstat-added { color: var(--fgColor-success); font-weight: var(--base-text-weight-semibold); }
.diffstat-deleted { color: var(--fgColor-danger); font-weight: var(--base-text-weight-semibold); }
.as-of.working { animation: fade 1.6s ease-in-out infinite; }
.run-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.run { font-size: var(--text-body-size-small); display: flex; flex-wrap: wrap; gap: var(--space-xs); align-items: center; }
.run-note { flex-basis: 100%; font-size: var(--text-caption-size); color: var(--fgColor-muted); overflow-wrap: anywhere; }
.sha { font-family: var(--fontStack-monospace); }

/* The watch line, tinted to match the card you clicked — a colour is only a
   colour until something tells you what it means, and this is the words.
   Primer's Flash shape: a muted ground with a border in the same role. */
.watch-line {
  font-size: var(--text-body-size-small); margin: 0;
  padding: var(--space-sm); border-radius: var(--borderRadius-medium);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  background: var(--bgColor-muted);
}
.watch-line .octicon { margin-right: var(--space-xs); vertical-align: text-bottom; }
/* The same two answers as the card, or the pane contradicts the card you clicked
   to open it. The per-state ICON stays — it is the reason, and here there is a
   sentence beside it to say what it means. */
.watch-line.watched-unreviewed,
.watch-line.watched-responded,
.watch-line.watched-changed,
.watch-line.watched-answer,
.watch-line.watched-merge {
  border-color: var(--borderColor-success-muted); background: var(--bgColor-success-muted);
}
.watch-line.watched-unreviewed .octicon,
.watch-line.watched-responded .octicon,
.watch-line.watched-changed .octicon,
.watch-line.watched-answer .octicon,
.watch-line.watched-merge .octicon { color: var(--fgColor-success); }
.watch-line.watched-rebased .octicon,
.watch-line.watched-waiting .octicon,
.watch-line.watched-merging .octicon { color: var(--fgColor-muted); }

/* ---- branch name + its icon-only copy button ---- */

/* .branch-name is Primer's, from components.css. It has no max width of its own
   because GitHub shows it inline in prose; in a 22rem pane it has to be told to
   truncate rather than push the copy button off the edge. */
.branch-row { display: flex; align-items: center; gap: var(--space-xs); min-width: 0; }
.branch-row .branch-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; word-break: normal;
}
/* Primer pulls the glyph back with a negative margin, which is tuned for a branch
   name sitting inside a sentence. Given its own row it just collides with the
   first character. */
.branch-name .octicon { margin: 0 var(--space-xxs) 0 0; }
/* Colour and pointer come from Primer's a.branch-name once the ref is an anchor.
   The decoration is ours: Primer leaves it to the host page, and the UA default
   underlines every anchor — GitHub's branch ref carries none at rest. */
a.branch-name { text-decoration: none; }
a.branch-name:hover { text-decoration: underline; }
a.branch-name:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: 1px; }

/* ---- the extra card: one slot, description as its floor ---- */

.extra-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-sm); }
.extra-head h4 { margin: 0 0 var(--space-xs); }
.extra { display: flex; flex-direction: column; gap: var(--space-xs); }

/* ---- the description, drawn as the comment it is ----
   GitHub's shape: the author's face outside the box, a header saying who wrote
   it, and a notch pointing from the box back at them. A PR description is the
   opening comment of the thread, and rendering it as an unattributed slab under
   a grey heading made it read as metadata about the PR rather than as somebody
   telling you what they did. */
.comment { display: flex; gap: var(--space-sm); align-items: flex-start; }
.comment-avatar { flex: 0 0 auto; padding-top: 0.15rem; }
/* Size comes from avatarHTML's width/height attributes, which also decide what
   resolution is REQUESTED from github.com — scaling a 16px avatar up to 28 in
   CSS would fetch the small one and render it soft. Only the shape is here. */
.comment-avatar img { border-radius: 50%; display: block; }
.comment-box {
  position: relative; flex: 1 1 auto; min-width: 0;
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  background: var(--bgColor-default);
}
/* The notch. Two triangles, the back one in the border colour and the front one
   in the fill, offset by a pixel — the standard way to draw a bordered arrow,
   and the reason it is two elements rather than one. */
.comment-box::before, .comment-box::after {
  content: ""; position: absolute; top: 0.7rem; right: 100%;
  border: 0.45rem solid transparent; border-left: 0;
}
.comment-box::before { border-right-color: var(--borderColor-default); }
.comment-box::after { border-right-color: var(--bgColor-default); margin-right: -1px; }
.comment-head {
  display: flex; align-items: baseline; gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  background: var(--bgColor-muted);
  border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0;
  font-size: var(--text-body-size-small); color: var(--fgColor-muted);
}
.comment-head a { color: var(--fgColor-default); text-decoration: none; }
.comment-head a:hover { color: var(--fgColor-accent); text-decoration: underline; }
/* Roomier than the board's usual padding: this is the one block on the page that
   is read rather than scanned, and 16px prose in a tight box reads worse than
   12px prose did. */
.comment-body { padding: var(--space-md); }

/* ---- the PR description ----

   .markdown-body is Primer's own, from components.css — the same stylesheet
   github.com renders a PR body with. It is built for a full-width page, so the
   only rules here are the ones that fit it into a 22rem pane: the type scale down
   one step, and no top margin fighting the section rule above it.

   The markdown is rendered by the SERVER (see internal/server/markdown.go) and
   arrives as HTML. Doing it there rather than here is what keeps this page free
   of a build step, and it is where the safety lives: raw HTML is omitted and
   unsafe link schemes are stripped in the AST, both covered by tests.

   bodyHtml is therefore trusted; the raw body never is, and is only ever used as
   escaped text if rendering came back empty. */
/* Reading size, not UI size — 14px.
   This was --text-body-size-small (12px), the size the BOARD is set in: right
   for a card you scan, wrong for prose you read.

   It is NOT --text-body-size-large. Primer's shorthand for that is annotated
   "User-generated content, markdown rendering", which reads like the answer and
   is not: the token resolves to 16px, and github.com renders a PR description at
   14px. Held side by side the 16px version was visibly larger than the thing it
   was copying. Primer's default and github.com's choice differ here, and the one
   worth matching is the page people compare this to. */
.markdown-body {
  font-size: var(--text-body-size-medium); line-height: var(--text-body-lineHeight-medium);
  overflow-wrap: anywhere;
  background: transparent;
}
/* Link colour. Primer's markdown.css styles only a:not([href]) — the colour of a
   real link comes from base.css, which sets `a { color: var(--fgColor-accent) }`
   for the whole page. This board does not vendor base.css (its resets would
   fight the rest of the stylesheet), so descriptions rendered their links in the
   browser's default blue, and visited ones in purple. Same class of assumption
   as .btn-octicon's missing cursor: Primer expects a host page that has already
   done this.

   Scoped to the description rather than made global, because every other link on
   the board already states its own colour, and a global a:hover underline would
   then have to be undone on the menu rows, the repo list and the labels. */
.markdown-body a { color: var(--fgColor-accent); text-decoration: none; }
.markdown-body a:hover { text-decoration: underline; }

.markdown-body > :first-child { margin-top: 0; }
.markdown-body > :last-child { margin-bottom: 0; }
.markdown-body h1, .markdown-body h2, .markdown-body h3,
.markdown-body h4, .markdown-body h5, .markdown-body h6 {
  /* One step above the body it heads (16px over 14px), rather than the flat run
     of same-size bold lines a 12px body forced. --text-title-size-medium is 20px
     and shouts on a panel this size. */
  font-size: var(--text-title-size-small); font-weight: var(--base-text-weight-semibold);
  padding-bottom: 0; border-bottom: none; margin: var(--space-md) 0 var(--space-xs);
}
/* The fallback when rendering produced nothing: escaped text, breaks preserved. */
.pr-body-raw {
  font-family: inherit; white-space: pre-wrap; margin: 0;
  font-size: var(--text-body-size-medium); line-height: var(--text-body-lineHeight-medium);
  color: var(--fgColor-default); overflow-wrap: anywhere;
}

/* ---- brand ---- */

.brand { display: flex; align-items: center; gap: var(--space-md); flex-wrap: wrap; }
/* The wordmark carries the brand; the title says which of Thirdfort's things
   this is. A rule between them keeps the two readings separate without needing
   a second type size to do it. */
/* Inlined rather than <img>-linked on purpose: an external SVG is its own
   document, so currentColor there resolves to black and the wordmark disappears
   on a dark ground. Inline, it inherits from the page. */
.brand-logo { height: 1.35rem; width: auto; color: var(--fgColor-default); display: block; }
.brand h1 {
  margin: 0; padding-left: var(--space-md);
  border-left: var(--borderWidth-thin) solid var(--borderColor-default);
  font-size: var(--text-body-size-medium); font-weight: var(--base-text-weight-medium); color: var(--fgColor-muted);
}
/* The repo sits inside the h1 so it inherits the title's size exactly, and is
   given the foreground colour: the product name is context, the repo is the
   thing you are actually looking at. That ordering is why the muted/bright split
   runs this way round rather than the other. */
/* The repo switcher, in GitHub's shape: owner in plain text, repository as the
   control. The board is one repo's, and moving between them was a trip back to
   "/" and a second click — a long way round for the thing people do most after
   reading a board. */
/* Breathing room around the slash, and no box until you reach for it. GitHub's
   switcher is text that happens to be clickable — a filled control with a border
   sitting in the title reads as a form field, and made the header look busier
   than the board it introduces. */
.repo-switch { position: relative; display: flex; align-items: center; gap: var(--space-sm); }
.repo-owner, .repo-sep { color: var(--fgColor-muted); }
.repo-trigger {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  padding: var(--space-xxs) var(--space-xs); margin-left: calc(-1 * var(--space-xs));
  background: none; color: var(--fgColor-default); border: 0;
  border-radius: var(--borderRadius-medium);
  /* The same size as the owner beside it. They are one path — "thirdfort /
     blue-skies" — and setting the second half larger made it read as a heading
     with a prefix. Weight carries the emphasis instead, which is what GitHub
     does. */
  font: inherit; font-weight: var(--base-text-weight-semibold); cursor: pointer;
}
.repo-trigger:hover { background: var(--control-bgColor-hover); }
.repo-trigger:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: -2px; }
.repo-trigger .octicon { margin: 0; color: var(--fgColor-muted); }
/* The chevron is a disclosure, not part of the name: a gap tells you the name
   ends before it does. */
.repo-trigger .octicon-chevron-down { margin-left: var(--space-xxs); }

.Overlay--repo .pop-head {
  margin: 0 0 var(--space-sm); font-weight: var(--base-text-weight-semibold);
  color: var(--fgColor-default);
}
.Overlay--repo .pop-note { margin: 0 0 var(--space-xs); color: var(--fgColor-muted); font-size: var(--text-body-size-small); }
.repo-search {
  width: 100%; margin-bottom: var(--space-sm); padding: var(--space-xs) var(--space-sm);
  background: var(--bgColor-default); color: var(--fgColor-default);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium); font: inherit;
}
.repo-search:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: -1px; }
.repo-switch-list { list-style: none; margin: 0; padding: 0; }
.repo-switch-list a {
  display: flex; align-items: center; gap: var(--space-xs);
  padding: var(--space-xs); border-radius: var(--borderRadius-medium);
  color: var(--fgColor-default); text-decoration: none;
}
.repo-switch-list a:hover { background: var(--control-bgColor-hover); }
/* A fixed-width column for the tick, so the names line up whether or not a row
   carries one — a list that shifts sideways by an icon is harder to scan than
   one with a gap in it. */
.repo-switch-list .tick { display: inline-flex; width: 16px; flex: none; color: var(--fgColor-default); }
.repo-switch-list .octicon { color: var(--fgColor-muted); margin: 0; }
.repo-switch-list .is-current a { font-weight: var(--base-text-weight-semibold); }

/* The wordmark goes home, as a site logo is expected to. Home is the repo
   picker, so this is the way back out of a board without editing the URL. */
.brand-home { display: block; line-height: 0; border-radius: var(--borderRadius-small); }
.brand-home:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: 3px; }
.brand-home:hover { opacity: 0.85; }

/* ---- overlays: the team breakdown and the viewer menu ----

   Primer's Overlay shape — the floating surface, its border and its shadow are
   all tokens. Plain absolute positioning inside a relative wrapper rather than
   the native popover API: native puts it in the top layer, which then needs CSS
   anchor positioning to sit under its trigger, and that is not yet everywhere. */

.Overlay-wrap { position: relative; flex: 0 0 auto; }
.Overlay {
  position: absolute; top: calc(100% + var(--space-xs)); right: 0; z-index: 30;
  background: var(--overlay-bgColor);
  border: var(--borderWidth-thin) solid var(--overlay-borderColor);
  border-radius: var(--borderRadius-large);
  box-shadow: var(--shadow-floating-small);
  text-align: left;
}
.Overlay--team { min-width: 20rem; max-width: min(28rem, 90vw); padding: var(--space-sm) var(--space-md); }
.Overlay--user { min-width: 15rem; padding: 0; overflow: hidden; }
/* Anchored LEFT, unlike the other two: this one hangs off the title at the far
   left of the header, where a right-anchored overlay would open off the page. */
.Overlay--repo {
  right: auto; left: 0; width: 22rem; max-height: 70vh; overflow: auto;
  padding: var(--space-sm);
}

/* Reads as GitHub's "View details" link rather than a button: it sits in a card
   corner, and a filled control there would compete with the figure. */
.team-trigger {
  font: inherit; font-size: var(--text-body-size-small); cursor: pointer;
  display: inline-flex; align-items: center; gap: var(--space-xs);
  padding: 0; color: var(--fgColor-accent);
  background: none; border: none;
}
.team-trigger:hover { text-decoration: underline; }
.team-trigger:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: 2px; }

/* Primer's ActionList — the menu rows inside the viewer overlay. */
.ActionList { list-style: none; margin: 0; padding: var(--space-xs); }
.ActionList-item {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--borderRadius-medium);
  font-size: var(--text-body-size-medium); color: var(--fgColor-default); text-decoration: none;
}
.ActionList-item:hover { background: var(--control-transparent-bgColor-hover); }
.ActionList-item .octicon { color: var(--fgColor-muted); }
.ActionList-item:hover .octicon { color: var(--fgColor-default); }
.ActionList-note { padding: var(--space-xs) var(--space-sm); font-size: var(--text-body-size-small); color: var(--fgColor-muted); }

/* ---- who you are, and the way out ---- */

/* The auto margin moved to .head-right, which now holds both this and the
   filters — two of them here would have split the free space between them and
   left a gap in the middle. */
.who { display: flex; align-items: center; }
/* Filters and account, pushed to the far end of the brand row together. */
.head-right { display: flex; align-items: center; gap: var(--space-md); margin-left: auto; flex-wrap: wrap; }
.user-trigger {
  display: inline-flex; align-items: center;
  padding: 0; background: none; border: var(--borderWidth-thin) solid transparent;
  border-radius: var(--borderRadius-full); cursor: pointer; line-height: 0;
}
.user-trigger:hover { border-color: var(--borderColor-emphasis); }
.user-trigger:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: 2px; }
/* Primer's avatar: a circle with a hairline, so it reads as an avatar even before
   the image loads and whatever the image's own edges are doing. */
.avatar {
  display: block; width: 28px; height: 28px; border-radius: var(--borderRadius-full);
  background: var(--bgColor-neutral-muted);
  box-shadow: 0 0 0 1px var(--avatar-borderColor);
}
.Overlay--user .id { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-md); }
.Overlay--user .id .avatar { width: 34px; height: 34px; }
.Overlay--user .id-text { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.Overlay--user .id-text strong { font-size: var(--text-body-size-medium); font-weight: var(--base-text-weight-semibold); color: var(--fgColor-default); }
.Overlay--user .id-text span { font-size: var(--text-body-size-small); color: var(--fgColor-muted); }
.Overlay--user .ActionList { border-top: var(--borderWidth-thin) solid var(--borderColor-muted); }

/* ---- build footer ----

   Small and muted because it is reference, not information you read on purpose —
   but always present, because the question it answers ("am I looking at a stale
   binary?") is one you only think to ask after wasting time on it. A long-running
   `prowl serve` keeps whatever binary it started with, so the page can look
   current while the process is hours behind. */
.foot {
  /* No auto margin any more: .board grows to fill the region, so this sits
     under it wherever the board ends — at the bottom of the window on a short
     board, and below the last card on a long one. */
  padding: var(--space-md) var(--space-xxs) 0;
  text-align: center;
  font-size: var(--text-caption-size);
  color: var(--fgColor-muted);
  user-select: text; /* so a version can be copied into a bug report */
}

/* The purr build beside prowl's. Same muted weight — it is chrome, not a
   warning — until it could not be asked, when it takes the attention colour: a
   board whose findings source is unreachable is showing reviews that may be
   stale or absent, and that is worth a glance's worth of colour. */
.foot .purr-build.is-unknown { color: var(--fgColor-attention); }

/* ---- signed-out / no-access gate ----

   Deliberately a page rather than an automatic redirect to /auth/login. Three
   states exist, not two: anonymous, signed-in-but-not-allowed, and in. Only the
   first is fixed by logging in — bouncing the second one back to GitHub, which
   has already authorised it, is an infinite loop. A shared URL pasted into Slack
   should also say what it is before throwing you at a consent screen. */
.gate {
  max-width: 30rem;
  margin: 12vh auto;
  padding: var(--space-xl);
  text-align: center;
  background: var(--bgColor-default);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-large);
  /* The page stopped scrolling when the board took over that job, and this is
     the other thing on it that can be taller than the viewport — the repo picker
     in an org with a lot of repositories. It scrolls itself rather than being
     clipped. min-height:0 for the same reason .scroller needs it: a flex item
     will not shrink below its content without being told it may. */
  flex: 0 1 auto; min-height: 0; overflow: auto;
}
.gate h2 { margin: 0 0 var(--space-sm); font: var(--text-title-shorthand-medium); }
.gate p { margin: 0 0 var(--space-lg); font-size: var(--text-body-size-medium); color: var(--fgColor-muted); }
.gate code { font-family: var(--fontStack-monospace); font-size: var(--text-codeInline-size); color: var(--fgColor-muted); }

/* Repo picker. Wider than the sign-in gate because it holds a list, not a
   sentence. */
.gate.gate-wide { max-width: 40rem; text-align: left; margin-top: 8vh; }
/* The heading and its refresh control on one line. The button sits with the
   heading rather than beside the list, because it reloads the whole list — a
   control next to the rows would read as filtering them. */
.gate-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.gate-head h2 { margin-bottom: var(--space-sm); }
/* When the list was fetched, in the sentence describing it. Quiet — it matters
   only when a count looks wrong. */
.repo-age { color: var(--fgColor-muted); }
.repo-filter {
  width: 100%; margin-bottom: var(--space-sm);
  padding: var(--space-xs) var(--space-md);
  font: inherit; font-size: var(--text-body-size-medium);
  color: var(--fgColor-default); background: var(--bgColor-default);
  border: var(--borderWidth-thin) solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
}
.repo-filter:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: -1px; }
.repo-list { list-style: none; margin: 0; padding: 0; max-height: 55vh; overflow-y: auto; }
.repo-list a {
  display: grid; grid-template-columns: auto 1fr auto; gap: 0 var(--space-sm);
  align-items: center;
  padding: var(--space-sm); border-radius: var(--borderRadius-medium);
  color: var(--fgColor-default); text-decoration: none;
}
.repo-list a:hover { background: var(--control-transparent-bgColor-hover); }
.repo-list a .octicon { color: var(--fgColor-muted); grid-row: 1; }
.repo-row-name { font-size: var(--text-body-size-medium); font-weight: var(--base-text-weight-medium); }
/* The open-PR count is why you would pick one board over another, so it earns
   its place on the row rather than hiding behind a click. */
.repo-prs { display: inline-flex; align-items: center; gap: var(--space-xxs); font-size: var(--text-body-size-small); color: var(--fgColor-muted); }
.repo-desc {
  grid-column: 2 / -1; font-size: var(--text-body-size-small); color: var(--fgColor-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Findings. Collapsed per finding so the list stays scannable: severity,
   location and the gist of each are visible, and only the prose and the fix are
   behind a click. High severity ships open — see findingHTML. */
/* Tight. Six findings is a list to scan, not six cards to consider one at a
   time — space between them slows that down without adding anything, because
   each row already has a border to separate it. */
/* The triage plane, above the findings it indexes. Severity up, confidence
   right — see triagePlaneHTML for what the four corners mean. */
.triage {
  display: block;
  width: 100%;
  max-width: 24rem;
  height: auto;
  margin: 0.5rem 0 0.2rem;
}
.triage-quad {
  font-size: 8px;
  fill: var(--fgColor-muted);
  font-family: inherit;
}
.triage-axis {
  font-size: 8px;
  fill: var(--fgColor-muted);
  font-family: inherit;
  letter-spacing: 0.02em;
}
/* Filled, not outlined: a dot has to carry a legible number at 9px radius, and
   an outline leaves the digit competing with the gridlines behind it. */
.triage-dot circle { fill: var(--fgColor-muted); }
.triage-dot--danger circle { fill: var(--fgColor-danger); }
.triage-dot--attention circle { fill: var(--fgColor-attention); }
.triage-dot text {
  font-size: 9px;
  font-weight: 600;
  font-family: inherit;
  fill: var(--fgColor-onEmphasis);
  pointer-events: none;
}
/* Hollow means unverified: the confidence number behind it is a default, not a
   measurement, so the dot must not look like a scored one. */
.triage-dot--unverified circle {
  fill: none;
  stroke: var(--fgColor-muted);
  stroke-width: 1.5;
}
.triage-dot--unverified.triage-dot--danger circle { stroke: var(--fgColor-danger); }
.triage-dot--unverified.triage-dot--attention circle { stroke: var(--fgColor-attention); }
.triage-dot--unverified text { fill: var(--fgColor-muted); }
.triage-dot--unverified.triage-dot--danger text { fill: var(--fgColor-danger); }
.triage-dot--unverified.triage-dot--attention text { fill: var(--fgColor-attention); }

.triage-legend { margin: 0 0 0.3rem; }
.triage-dot { cursor: pointer; }
.triage-dot:hover circle { r: 11; }
.triage-dot:focus-visible { outline: 2px solid var(--fgColor-accent); outline-offset: 2px; }

/* The list's half of the shared numbering. Tabular so 9 and 10 line up. */
.finding-n {
  flex: none;
  min-width: 1.1rem;
  font-variant-numeric: tabular-nums;
  font-size: 0.72rem;
  color: var(--fgColor-muted);
  text-align: right;
}

.finding-list { list-style: none; margin: 0.4rem 0 0; padding: 0; display: grid; gap: 0.15rem; }
.finding {
  /* A grid item defaults to min-width:auto, which refuses to shrink below its
     content — so a long finding pushed the pane sideways instead of wrapping.
     The fix belongs on the item, not the text. */
  min-width: 0;
  border: 1px solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  background: var(--bgColor-muted);
}
.finding summary {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  list-style: none; /* the default marker crowds a row this tight */
}
.finding summary::-webkit-details-marker { display: none; }
.finding summary:hover { background: var(--control-bgColor-hover); border-radius: var(--borderRadius-medium); }
.finding summary:focus-visible { outline: 2px solid var(--fgColor-accent); outline-offset: -2px; }

/* The gist is a hint at what is inside, not the content — it competes with the
   severity and the location if it is as bright as they are, and those are what
   the row is FOR. Muted and smaller, so the row reads as "high, in this file"
   with the sentence trailing behind. */
.finding-gist {
  /* Shrinks a hundred times faster than its neighbours, so it is the first thing
     to give up space and can vanish entirely before the location loses a
     character. It was `1 1 auto`, an equal shrink factor with the path beside it
     — and since flex shrinks proportionally to base size, the LONGER item lost
     more. A deep path with a long gist next to it collapsed to "…:78", which
     identifies nothing at all, while the gist it was competing with is a preview
     of text you get in full the moment you open the row. */
  flex: 1 100 auto; min-width: 0;
  font-size: 0.72rem;
  color: var(--fgColor-muted);
  opacity: 0.75;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The specialist that raised it: small and muted like the gist, but it keeps
   its width, since the gist is the thing that gives way. */
.finding-by {
  flex: none; font-size: 0.72rem; color: var(--fgColor-muted);
  white-space: nowrap;
}
/* The attribute is on the <details>, not the <li> that carries .finding. */
.finding details[open] .finding-gist { display: none; }

/* The location links to the file at the reviewed commit. Styled as a link
   because it is one — a monospace string that navigates but does not look like
   it is a thing people do not click. */
.finding-where {
  /* Shrinkable, and truncated from the LEFT.
     flex:0 0 auto was a horizontal overflow waiting for a long enough path, and
     this repo supplies them —
     client/.../workflows/checks/batchunassignreviewtasks/latest/workflow.go:14
     pushed the whole pane sideways. Two columns would only make it likelier.
     The tail is the part that identifies a finding: the filename and the line.
     A path elided at the right ("client/services/internal/…") tells you nothing
     you did not already know, so the ellipsis goes at the start instead —
     direction:rtl moves where the overflow is taken from, and text-align:left
     keeps the text where the eye expects it. Safe for these strings: they begin
     with a letter and end with a digit, so no neutral character sits at either
     edge where the bidi algorithm could reorder it. The title attribute carries
     the whole path for anyone who needs it. */
  /* A floor, in characters: enough for a filename and a line number, which is
     the least that still names a finding. Below that the row would rather
     overflow-hide than lie about which file it means. */
  flex: 0 1 auto; min-width: min(22ch, 100%);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  direction: rtl; text-align: left;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  color: var(--fgColor-accent);
  text-decoration: none;
}
a.finding-where:hover { text-decoration: underline; }

/* The finding's prose is reading text, at reading size — the same argument as
   .markdown-body. The summary ROW stays small: that is a scannable index, and
   the row and the paragraph it opens are doing different jobs.
   overflow-wrap:anywhere, because findings quote identifiers and paths that have
   no spaces to break at — a word that cannot break is a line that cannot wrap. */
.finding-body {
  padding: 0.15rem 0.55rem 0.5rem;
  font-size: var(--text-body-size-medium); line-height: var(--text-body-lineHeight-medium);
  overflow-wrap: anywhere;
}

/* The drafted comment. This is the text purr would post, so it is drawn as a
   comment rather than an indented paragraph — indentation alone gave no reason
   for the text to be set apart, which is why it read as a rendering fault. */
.draft {
  margin: 0 0.55rem 0.55rem;
  border: 1px solid var(--borderColor-default);
  border-radius: var(--borderRadius-medium);
  overflow: hidden;
}
.draft-head {
  display: flex; align-items: center; gap: 0.35rem;
  padding: 0.3rem 0.5rem;
  background: var(--bgColor-muted);
  border-bottom: 1px solid var(--borderColor-default);
  font-size: 0.7rem; color: var(--fgColor-muted);
}
.draft-body {
  padding: 0.45rem 0.5rem;
  font-size: var(--text-body-size-medium); line-height: var(--text-body-lineHeight-medium);
  overflow-wrap: anywhere;
}
